3. 創(chuàng)建新線程
創(chuàng)建一個線程有兩種方法:
- 從Thread類繼承并重寫run()方法沼侣。創(chuàng)建一個實例,調用start()方法,這會在新線程上調用run()。如下:
Thread t = new Thread() { // Create an instance of an anonymous inner class that extends Thread
@Override
public void run() { // Override run() to specify the running behaviors
for (int i = 0; i < 100000; ++i) {
if (stop) break;
tfCount.setText(count + "");
++count;
// Suspend itself and yield control to other threads for the specified milliseconds
// Also provide the necessary delay
try {
sleep(10); // milliseconds
} catch (InterruptedException ex) {}
}
}
};
t.start(); // Start the thread. Call back run() in a new thread
- 創(chuàng)建一個實現(xiàn)Runnable 接口的類并為抽象方法 run()提供具體實現(xiàn)动知。用帶Runnable對象的構造函數(shù)構造一個新線程實例并調用 start()方法,這會在新線程上調用run()员辩。
// Create an anonymous instance of an anonymous inner class that implements Runnable
// and use the instance as the argument of Thread's constructor.
Thread t = new Thread(new Runnable() {
// Provide implementation to abstract method run() to specify the running behavior
@Override
public void run() {
for (int i = 0; i < 100000; ++i) {
if (stop) break;
tfCount.setText(count + "");
++count;
// Suspend itself and yield control to other threads
// Also provide the necessary delay
try {
Thread.sleep(10); // milliseconds
} catch (InterruptedException ex) {}
}
}
});
t.start(); // call back run() in new thread
因為Java不支持多重繼承盒粮,一般選擇第二種方法。如果類已經(jīng)繼承了某一超類屈暗,它便不能繼承Thread,必須實現(xiàn) Runnable 接口脂男。第二種方法也能用來提供針對JDK 1.1的兼容养叛。應該注意的是Thread類本身實現(xiàn)Runnable接口。
run()方法制定線程的任務宰翅。不能直接從程序中調用run()方法弃甥,而是先要創(chuàng)建線程實例并調用start()方法,之后start()方法在新線程上調用run()汁讼。
3.1 Runnable接口
接口java.lang.Runnable 聲明一個抽象方法run()用來制定線程的執(zhí)行任務:public void run();
3.2 Thread類
類 java.lang.Thread 有以下構造函數(shù):
public Thread();
public Thread(String threadName);
public Thread(Runnable target);
public Thread(Runnable target, String threadName);
前面兩個構造函數(shù)用來通過子類化Thread 類創(chuàng)建線程淆攻,下面兩個用來創(chuàng)建帶實現(xiàn)Runnable 接口類實例的線程。
Thread類實現(xiàn)Runnable接口嘿架,如圖所示:
如上面所提到的瓶珊,run()方法制定線程的執(zhí)行任務。不能直接調用run()方法而應該調用Thread類的 start()方法耸彪。如果線程是通過繼承Thread 類創(chuàng)建的伞芹,方法start()將調用繼承類中重寫的run()方法;如果線程是通過給線程構造函數(shù)提供Runnable 對象創(chuàng)建的蝉娜,方法start()將調用Runnable對象的run()方法 (而非Thread的run())唱较。
3.3 通過繼承 Thread 并重寫run()創(chuàng)建新線程
通過繼承Thread類來創(chuàng)建并運行一個新線程:
- 定義一個繼承父類Thread的子類(命名或者匿名);
- 在子類中重寫run()方法來制定線程的操作召川,(并提供其他如構造函數(shù)南缓,變量和方法的實現(xiàn));
- Client類創(chuàng)建了一個該新類的實例荧呐, 該實例為Runnable對象(因為 Thread類本身實現(xiàn)Runnable接口)汉形;
- Client類調用Runnable對象的start()方法纸镊。結果是兩個線程并發(fā)執(zhí)行-調用start()之后的當前線程和執(zhí)行Runnable對象 run()方法的新線程。
class MyThread extends Thread {
// override the run() method
@Override
public void run() {
// Thread's running behavior
}
// constructors, other variables and methods
......
}
public class Client {
public static void main(String[] args) {
......
// Start a new thread
MyThread t1 = new MyThread();
t1.start(); // Called back run()
......
// Start another thread
new MyThread().start();
......
}
}
通常使用內部類 (命名或匿名)而非普通子類获雕,這是為了可讀性并獲取對外部類私有變量和方法的訪問薄腻。例如:
public class Client {
......
public Client() {
Thread t = new Thread() { // Create an anonymous inner class extends Thread
@Override
public void run() {
// Thread's running behavior
// Can access the private variables and methods of the outer class
}
};
t.start();
...
// You can also used a named inner class defined below
new MyThread().start();
}
// Define a named inner class extends Thread
class MyThread extends Thread {
public void run() {
// Thread's running behavior
// Can access the private variables and methods of the outer class
}
}
}
public class MyThread extends Thread {
private String name;
public MyThread(String name) { // constructor
this.name = name;
}
// Override the run() method to specify the thread's running behavior
@Override
public void run() {
for (int i = 1; i <= 5; ++i) {
System.out.println(name + ": " + i);
yield();
}
}
}
通過繼承Thread類創(chuàng)建一個MyThead類并重寫run()方法。定義一個帶字符串參數(shù)的構造函數(shù)届案,該字符串用來標識此線程的名字庵楷。run()方法打印數(shù)字1 到5, 調用yield()讓線程在打印出每個數(shù)字之后自動將資源控制權交與其他線程。
public class TestMyThread {
public static void main(String[] args) {
Thread[] threads = {
new MyThread("Thread 1"),
new MyThread("Thread 2"),
new MyThread("Thread 3")
};
for (Thread t : threads) {
t.start();
}
}
}
測試類分配并啟動三個線程楣颠。輸出如下:
Thread 1: 1
Thread 3: 1
Thread 1: 2
Thread 2: 1
Thread 1: 3
Thread 3: 2
Thread 2: 2
Thread 3: 3
Thread 1: 4
Thread 1: 5
Thread 3: 4
Thread 3: 5
Thread 2: 3
Thread 2: 4
Thread 2: 5
注意輸出是不確定的(每次運行都可能產生不同輸出), 因為沒有完全控制線程的執(zhí)行方式尽纽。
3.4 通過實現(xiàn)Runnable接口創(chuàng)建新線程
通過實現(xiàn)Runnable接口創(chuàng)建并運行新線程:
1.定義一個實現(xiàn)Runnable接口的類;
2.在類中為抽象方法run()提供實現(xiàn)來制定線程的操作童漩,(并提供其他如構造函數(shù)弄贿,變量和方法的實現(xiàn));
3.Client類創(chuàng)建了一個該新類的實例矫膨,該實例稱作Runnable對象差凹;
4.Client類接著創(chuàng)建一個新線程對象,構造函數(shù)以Runnable對象為參數(shù)侧馅,然后調用start()方法危尿。Start()調用Runnable對象(而不是Thread類)中的 run()方法。
class MyRunnable extends SomeClass implements Runnable {
// provide implementation to abstract method run()
@Override
public void run() {
// Thread's running behavior
}
......
// constructors, other variables and methods
}
public class Client {
......
Thread t = new Thread(new MyRunnable());
t.start();
...
}
同樣地馁痴,內部類 (命名或匿名) 通常是為了可讀性并能獲取對外部類私有變量和方法的訪問谊娇。例如:
Thread t = new Thread(new Runnable() { // Create an anonymous inner class that implements Runnable interface
public void run() {
// Thread's running behavior
// Can access the private variables and methods of the outer class
}
});
t.start();
3.5 Thread類中的方法
Thread類中的方法包括:
-
public void start()
: 開始一個新線程。JRE 調用該類run()方 法罗晕,當前線程繼續(xù)運行济欢; -
public void run()
: 制定新線程的執(zhí)行任務。當run()完成時小渊,線程終止法褥; - 中斷函數(shù)
public static sleep(long millis)throws InterruptedException
public static sleep(long millis, int nanos)throws InterruptedException
public void interrupt()
暫停當前線程并將資源控制權交與其他線程以給定的時間。Sleep()方法是線程安全的因為它不會釋放自己的監(jiān)視器酬屉。通過調用 interrupt()方法能在給定時間到達之前喚醒阻塞線程挖胃。被喚醒線程會拋出InterruptedException并在繼續(xù)操作之前執(zhí)行其InterruptedException 處理函數(shù)。這是個靜態(tài)方法且廣泛用來暫停當前線程(通過Thread.sleep())梆惯,這樣其他線程就有機會執(zhí)行酱鸭。 這也在許多應用中提供了必要的延遲。例如:
try {
// Suspend the current thread and give other threads a chance to run
// Also provide the necessary delay
Thread.sleep(100); // milliseconds
} catch (InterruptedException ex) {}
-
public static void yield()
: 暗示調度程序當前線程愿意將當前處理器的使用權交與其他線程垛吗,但是該調度程序可以隨意忽略這條暗示凹髓,因此很少使用; -
public boolean isAlive()
: 如果線程是新建的或死亡返回false怯屉;如果線程是"就緒" 或 "阻塞"返回true蔚舀; -
public void setPriority(sint p)
: 設置線程的優(yōu)先級饵沧,依賴于運行情況。
stop(),suspend(),和resume()方法在JDK 1.4中已經(jīng)被廢棄了赌躺,它們因monitor的釋放而非線程安全狼牺,詳見JDK API文檔。
3.6 守護線程
有兩種線程礼患,分別是守護線程和用戶線程是钥。守護線程能通過方法setDaemon(boolean on)設置。守護線程是基礎線程缅叠,如:垃圾回收線程及 GUI的事件分發(fā)線程悄泥。JVM在運行線程都是守護線程的時候退出。換言之肤粱,當不再有用戶線程且所有存留線程都是基礎線程時弹囚,JVM 認為其任務已完成。
3.7 線程的生命周期
線程剛創(chuàng)建的時候處于"新建"(NEW)狀態(tài)领曼。在該狀態(tài)的時候鸥鹉,它只是堆棧中的一個對象,而沒有分配任何系統(tǒng)資源來執(zhí)行庶骄。從"新建"狀態(tài)開始毁渗,能做的唯一事情就是調用start()方法,這將線程置于"就緒"(RUNNABLE)狀態(tài)瓢姻。調用除start()之外的任何方法會導致IllegalThreadStateException祝蝠。
start()方法分配必需的系統(tǒng)資源來執(zhí)行線程音诈,安排線程運行并調用run()幻碱。 這將線程置于"就緒"狀態(tài)。由于大多數(shù)計算機只有一個CPU且通過CPU時間片來支持多線程细溅,因此在"就緒"狀態(tài)下褥傍,線程可能運行或等待CPU時間片。
線程不能被啟動兩次喇聊,否則會造成 IllegalThreadStateException恍风。
當以下事件出現(xiàn)時線程會進入"阻塞"狀態(tài):
- 調用sleep()掛起線程一段時間以將控制權交給其他線程,也可以調用 yield()來示意調度函數(shù)當前線程愿意交出當前處理器的使用權誓篱。只是調度函數(shù)會隨意忽略該暗示朋贬;
- 調用wait()方法等待滿足特定條件;
- 線程阻塞窜骄,并等待I/O操作的完成锦募。
線程再次從"阻塞"狀態(tài)變成 "就緒":
- 如果線程休眠,特定的休眠時間結束或休眠通過interrupt()方法中斷邻遏;
- 如果線程通過wait()處于等待狀態(tài)糠亩,調用notify()或者notifyAll()方法通知等待線程條件已經(jīng)滿足并結束等待虐骑;
- 阻塞線程的I/O操作完成。
只有當run()方法自然終止并退出時線程才處于“終止”狀態(tài)赎线。
方法isAlive()能被用來檢測線程是否存活廷没。如果線程是"新建" 或 "終止",isAlive()返回false垂寥;如果線程是"就緒"或"阻塞"則返回true颠黎。
JDK 1.5引入了一新方法getState(),該方法返回 (封裝) Thread.State枚舉類型矫废,為五種狀態(tài)中一種 - {NEW, BLOCKED, RUNNABLE, TERMINATED, WAITING}盏缤。
- NEW: 線程尚未啟動;
- RUNNABLE:
- WAITING:
- BLOCKED: 線程阻塞等待監(jiān)視器鎖蓖扑;
- TIMED_WAITING: 線程等待一定時間唉铜;
- TERMINATED:
4. 線程調度及優(yōu)先級
JVM 實現(xiàn)固定的線程調度機制。每個線程都會設置一個優(yōu)先級(在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之間)律杠。數(shù)字越大潭流,線程優(yōu)先級越高。當創(chuàng)建新線程柜去,它會繼承創(chuàng)建線程的優(yōu)先級灰嫉。可以調用方法setPriority()來改變線程的優(yōu)先級:public void setPriority(int priority);
參數(shù)priority取值范圍為1(優(yōu)先級最低)to 10嗓奢。
JVM選擇執(zhí)行優(yōu)先級最高的線程讼撒。如果有一個以上線程都有最高優(yōu)先級,JVM會遵循輪詢調度股耽。
JVM實行優(yōu)先權調度機制根盒,在這種環(huán)境下如果任何時候有一更高優(yōu)先級的線程狀態(tài)轉為"就緒",當前低優(yōu)先級線程將立即讓步與更高優(yōu)先級線程物蝙。
如果有兩個以上線程處于同一優(yōu)先級且均處于就緒狀態(tài)炎滞,線程可能會一直運行直至完成而不讓步與其他同優(yōu)先級線程,這樣會造成線程饑餓诬乞。因此通過sleep()或 yield()讓步與其他同級線程是個不錯的做法册赛。但是,永遠都不可能將控制權交與低優(yōu)先級線程震嫉。
在一些操作系統(tǒng)如Windows中森瘪,每一個運行線程都被分配一定量的CPU時間片,該時間片用以阻止其他同優(yōu)先級線程出現(xiàn)饑餓票堵。但不要依賴時間片扼睬,因為它也依賴于運行情況。
因此换衬,正在運行的線程在出現(xiàn)以下情況時會停止執(zhí)行:
- 更高優(yōu)先級線程狀態(tài)為"就緒"痰驱;
- 正在運行線程通過調用如:sleep()证芭,yield()和wait()等方法主動放棄控制;
- 正在運行線程終止担映,即其run()方法退出废士;
- 在執(zhí)行時間片的系統(tǒng)上,運行線程消耗完自己的CPU時間片蝇完;
需要注意的一點是線程調度和優(yōu)先級都依賴于JVM官硝,因為JVM是虛擬機并需要原生操作系統(tǒng)資源來支持多線程。大部分JVM不保證優(yōu)先級最高的線程一直在運行短蜕,有時為了避免饑餓會選擇執(zhí)行較低優(yōu)先級線程氢架,因此不應該依賴算法中的優(yōu)先次序。
5. 監(jiān)視器鎖 & 同步
監(jiān)視器(monitor)是用來阻塞并喚醒線程的對象朋魔,在 java.lang.Object中通過以下機制支持:
- 每個對象都有一個鎖岖研;
- 關鍵字synchronized用來獲取對象鎖;
- java.lang.Object中的方法wait()警检,notify()和notifyAll()方法控制線程孙援。
每個java對象都有鎖。任何時候扇雕,鎖最多被一單線程控制拓售。可以對方法或某段代碼使用關鍵字synchronized镶奉。要執(zhí)行對象同步代碼的線程必須先要獲取該對象的鎖础淤。如果該鎖為其他線程所控制,那么嘗試的線程會進入“找鎖”狀態(tài)哨苛,且僅當鎖可以訪問的時候才準備完成鸽凶。占用鎖的線程執(zhí)行完同步代碼之后便會放棄鎖。
5.1 關鍵詞"synchronized"
如下:
public synchronized void methodA() { ...... } // synchronized a method based on this object
public void methodB() {
synchronized(this) { // synchronized a block of codes based on this object
......
}
synchronized(anObject) { // synchronized a block of codes based on another object
......
}
......
}
同步能在方法層面或區(qū)塊層面進行控制移国。變量不能同步吱瘩,需要同步所有訪問那個變量的方法道伟。
private static int counter = 0;
public static synchronized void increment() {
++counter;
}
public static synchronized void decrement() {
--counter;
}
也可以對靜態(tài)方法使用synchronized
迹缀。這情況下,需要獲取類鎖 (而不是實例鎖) 來執(zhí)行方法蜜徽。
public class SynchronizedCounter {
private static int count = 0;
public synchronized static void increment() {
++count;
System.out.println("Count is " + count + " @ " + System.nanoTime());
}
public synchronized static void decrement() {
--count;
System.out.println("Count is " + count + " @ " + System.nanoTime());
}
}
public class TestSynchronizedCounter {
public static void main(String[] args) {
Thread threadIncrement = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; ++i) {
SynchronizedCounter.increment();
try {
sleep(1);
} catch (InterruptedException e) {}
}
}
};
Thread threadDecrement = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; ++i) {
SynchronizedCounter.decrement();
try {
sleep(1);
} catch (InterruptedException e) {}
}
}
};
threadIncrement.start();
threadDecrement.start();
}
}
Count is -1 @ 71585106672577
Count is 0 @ 71585107040916
Count is -1 @ 71585107580661
Count is 0 @ 71585107720865
Count is 1 @ 71585108577488
Count is 0 @ 71585108715261
Count is 1 @ 71585109590928
Count is 0 @ 71585111400613
Count is 1 @ 71585111640095
Count is 0 @ 71585112581002
Count is 1 @ 71585112748760
Count is 2 @ 71585113580259
Count is 1 @ 71585113729378
Count is 2 @ 71585114579922
Count is 1 @ 71585114712832
Count is 2 @ 71585115578775
Count is 1 @ 71585115722626
Count is 2 @ 71585116578843
Count is 1 @ 71585116719452
Count is 0 @ 71585117583368
需要注意的重要一點是當對象被鎖住的時候祝懂,同步的方法和代碼都會被阻塞。而非同步方法不受鎖影響繼續(xù)執(zhí)行拘鞋,因此有必要同步所有操作共享資源的方法砚蓬。比如,如需要對一變量訪問進行同步盆色,那么所有指向該變量方法都應被同步灰蛙。否則祟剔,非同步方法不需先獲取鎖而直接訪問會影響該變量的狀態(tài)。
5.2 wait(), notify() & notifyAll() 實現(xiàn)線程間同步
這些方法都定義在java.lang.Object 類中(而不是java.land.Thread)摩梧,這些只能在同步代碼中調用物延。
方法wait()和notify()為共享對象提供了一種暫停線程,在合適的時候繼續(xù)該線程的方法仅父。
例子: 消費者和生產者
在本例中叛薯,生產者生產信息(通過方法putMessage()),該信息在下一條信息生產出來之前被消費者消費 (通過方法getMessage())笙纤。在這樣一個producer-consumer模式中耗溜,一個線程調用wait()將自己掛起(并釋放鎖),直到另一線程調用notify()或notifyAll()將其喚醒省容。
// Testing wait() and notify()
public class MessageBox {
private String message;
private boolean hasMessage;
// producer
public synchronized void putMessage(String message) {
while (hasMessage) {
// no room for new message
try {
wait(); // release the lock of this object
} catch (InterruptedException e) { }
}
// acquire the lock and continue
hasMessage = true;
this.message = message + " Put @ " + System.nanoTime();
notify();
}
// consumer
public synchronized String getMessage() {
while (!hasMessage) {
// no new message
try {
wait(); // release the lock of this object
} catch (InterruptedException e) { }
}
// acquire the lock and continue
hasMessage = false;
notify();
return message + " Get @ " + System.nanoTime();
}
}
public class TestMessageBox {
public static void main(String[] args) {
final MessageBox box = new MessageBox();
Thread producerThread = new Thread() {
@Override
public void run() {
System.out.println("Producer thread started...");
for (int i = 1; i <= 6; ++i) {
box.putMessage("message " + i);
System.out.println("Put message " + i);
}
}
};
Thread consumerThread1 = new Thread() {
@Override
public void run() {
System.out.println("Consumer thread 1 started...");
for (int i = 1; i <= 3; ++i) {
System.out.println("Consumer thread 1 Get " + box.getMessage());
}
}
};
Thread consumerThread2 = new Thread() {
@Override
public void run() {
System.out.println("Consumer thread 2 started...");
for (int i = 1; i <= 3; ++i) {
System.out.println("Consumer thread 2 Get " + box.getMessage());
}
}
};
consumerThread1.start();
consumerThread2.start();
producerThread.start();
}
}
Consumer thread 1 started...
Producer thread started...
Consumer thread 2 started...
Consumer thread 1 Get message 1 Put @ 70191223637589 Get @ 70191223680947
Put message 1
Put message 2
Consumer thread 2 Get message 2 Put @ 70191224046855 Get @ 70191224064279
Consumer thread 1 Get message 3 Put @ 70191224164772 Get @ 70191224193543
Put message 3
Put message 4
Consumer thread 2 Get message 4 Put @ 70191224647382 Get @ 70191224664401
Put message 5
Consumer thread 2 Get message 5 Put @ 70191224939136 Get @ 70191224965070
Consumer thread 1 Get message 6 Put @ 70191225071236 Get @ 70191225101222
Put message 6
輸出信息 (System.out)可能是亂序的抖拴,仔細檢查put/get時間戳可確證正確的操作順序。
同步的生產者方法putMessage()獲取對象鎖腥椒,檢查之前信息是否已被清理城舞。如果沒有清理則調用wait(),釋放對象鎖并進入WAITING狀態(tài)寞酿,將該線程置于對象的"等待"組家夺。另一方面,同步的消費者方法getMessage()獲取該對象鎖并檢查新信息伐弹。如果有一條新信息拉馋,清理之并調用notify(),這會在該對象的"等待"組上任意選擇一個線程(在本例中剛好是生產者線程)并將其置于BLOCKED狀態(tài)惨好。消費者線程相應地進入WAITING狀態(tài)并將自己置于對象的"等待"組(在wait()方法之后)煌茴。生產者線程之后獲取鎖并繼續(xù)執(zhí)行后面操作。
notify()和notifyAll()的區(qū)別在于:notify()從該對象的等待池中隨意選擇一個線程并將其置于找鎖狀態(tài)日川;而notifyAll()喚醒對象等待池中所有線程蔓腐。然后被喚醒的線程以正常方式競爭執(zhí)行。
多線程在類java.lang.Object處編入Java語言龄句,同步鎖保存在Object中回论,用來協(xié)調線程的方法wait(),notify()分歇,notifyAll()正好都在在類Object中傀蓉。
帶超時參數(shù)的wait()
下面幾種形式的wait()方法,帶一個超時參數(shù):
public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
超過超時時間線程將也進入BLOCKED狀態(tài)职抡。
5.3 饑餓與死鎖
饑餓(Starvation )指的是一個(或更多)線程不能訪問對象的狀態(tài)葬燎。此問題可通過對所有線程設置正確的優(yōu)先級加以解決。
死鎖(Deadlock)指的是一個線程正在等待一個條件,但是程序的其他地方阻止了該條件的滿足谱净,因而阻止了線程的執(zhí)行窑邦。典型的例子,也稱"死亡擁抱":線程1持有對象A的鎖壕探,線程2持有對象B的鎖奕翔。線程1等待獲取對象B的鎖,而線程2則等待獲取對象A的鎖浩蓉。兩個線程都處于死鎖狀態(tài)而不能繼續(xù)執(zhí)行派继。如果兩個線程都以相同順序找鎖,這種情況就不會出現(xiàn)捻艳。但是在程序中實現(xiàn)這種安排較為復雜驾窟。可以選擇的方法是要么在另一對象(而不是對象A或B)上同步认轨;要么只同步方法的一部分绅络,而非整個方法。死鎖可能會很復雜嘁字,因為它可能涉及到很多線程和對象且難于檢測恩急。
翻譯自:
https://www3.ntu.edu.sg/home/ehchua/programming/java/j5e_multithreading.html