轉(zhuǎn):《Java并發(fā)編程的藝術(shù)》
1 線程簡介
現(xiàn)代操作系統(tǒng)在運(yùn)行一個(gè)程序時(shí)狡汉,會(huì)為其創(chuàng)建一個(gè)進(jìn)程娄徊。例如,啟動(dòng)一個(gè)Java程序盾戴,操作
系統(tǒng)就會(huì)創(chuàng)建一個(gè)Java進(jìn)程〖娜瘢現(xiàn)代操作系統(tǒng)調(diào)度的最小單元是線程,也叫輕量級(jí)進(jìn)程(Light Weight Process)尖啡,在一個(gè)進(jìn)程里可以創(chuàng)建多個(gè)線程橄仆,這些線程都擁有各自的計(jì)數(shù)器、堆棧和局部變量等屬性衅斩,并且能夠訪問共享的內(nèi)存變量盆顾。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時(shí)執(zhí)行畏梆。
2 線程優(yōu)先級(jí)
現(xiàn)代操作系統(tǒng)基本采用時(shí)分的形式調(diào)度運(yùn)行的線程您宪, 操作系統(tǒng)會(huì)分出一個(gè)個(gè)時(shí)間片奈懒, 線程會(huì)分配到若干時(shí)間片, 當(dāng)線程的時(shí)間片用完了就會(huì)發(fā)生線程調(diào)度宪巨, 并等待著下次分配
磷杏。 線程分配到的時(shí)間片多少也就決定了線程使用處理器資源的多少, 而線程優(yōu)先級(jí)就是決定線程需要多或者少分配一些處理器資源的線程屬性捏卓。
在Java線程中极祸,通過一個(gè)整型成員變量priority來控制優(yōu)先級(jí), 優(yōu)先級(jí)的范圍從1~10怠晴, 在線程構(gòu)建的時(shí)候可以通過setPriority(int)方法來修改優(yōu)先級(jí)贿肩, 默認(rèn)優(yōu)先級(jí)是5, 優(yōu)先級(jí)高的線程分配時(shí)間片的數(shù)量要多于優(yōu)先級(jí)低的線程龄寞。 設(shè)置線程優(yōu)先級(jí)時(shí), 針對(duì)頻繁阻塞(休眠或者I/O操作) 的線程需要設(shè)置較高優(yōu)先級(jí)汤功, 而偏重計(jì)算(需要較多CPU時(shí)間或者偏運(yùn)算) 的線程則設(shè)置較低的優(yōu)先級(jí)物邑, 確保處理器不會(huì)被獨(dú)占
。 在不同的JVM以及操作系統(tǒng)上滔金, 線程規(guī)劃會(huì)存在差異色解,有些操作系統(tǒng)甚至?xí)雎詫?duì)線程優(yōu)先級(jí)的設(shè)定(因此,在程序中設(shè)置線程優(yōu)先級(jí)的實(shí)踐意義并不大餐茵,因?yàn)榫€程優(yōu)先級(jí)的最終解釋權(quán)在底層操作系統(tǒng))科阎。
3 并發(fā)與并行的區(qū)別
如果某個(gè)系統(tǒng)支持兩個(gè)或者多個(gè)操作(Action)同時(shí)存在,那么這個(gè)系統(tǒng)就是一個(gè)并發(fā)系統(tǒng)
忿族。如果某個(gè)系統(tǒng)支持兩個(gè)或者多個(gè)動(dòng)作同時(shí)執(zhí)行锣笨,那么這個(gè)系統(tǒng)就是一個(gè)并行系統(tǒng)
。并發(fā)系統(tǒng)與并行系統(tǒng)這兩個(gè)定義之間的關(guān)鍵差異在于“存在”這個(gè)詞道批。
在并發(fā)程序中可以同時(shí)擁有兩個(gè)或者多個(gè)線程错英。這意味著,如果程序在單核處理器上運(yùn)行隆豹,那么這兩個(gè)線程將交替地?fù)Q入或者換出內(nèi)存椭岩。這些線程是同時(shí)“存在”的——每個(gè)線程都處于執(zhí)行過程中的某個(gè)狀態(tài)
。如果程序能夠并行執(zhí)行璃赡,那么就一定是運(yùn)行在多核處理器上判哥。此時(shí),程序中的每個(gè)線程都將分配到一個(gè)獨(dú)立的處理器核上
碉考,因此可以同時(shí)運(yùn)行塌计。
“并行”概念是“并發(fā)”概念的一個(gè)子集。也就是說侯谁,你可以編寫一個(gè)擁有多個(gè)線程或者進(jìn)程的并發(fā)程序夺荒,但如果沒有多核處理器來執(zhí)行這個(gè)程序瞒渠,那么就不能以并行方式來運(yùn)行代碼
。因此技扼,凡是在求解單個(gè)問題時(shí)涉及多個(gè)執(zhí)行流程的編程模式或者執(zhí)行行為伍玖,都屬于并發(fā)編程的范疇。
用一個(gè)極其簡單的生活實(shí)例來解釋如下:
你吃飯吃到一半剿吻,電話來了窍箍,你一直到吃完了以后才去接,這就說明你不支持并發(fā)也不支持并行丽旅。
你吃飯吃到一半椰棘,電話來了,你停了下來接了電話榄笙,接完后繼續(xù)吃飯邪狞,這說明你支持并發(fā)。
你吃飯吃到一半茅撞,電話來了帆卓,你一邊打電話一邊吃飯,這說明你支持并行米丘。
并發(fā)的關(guān)鍵是你有處理多個(gè)任務(wù)的能力剑令,不一定要同時(shí)。并行的關(guān)鍵是你有同時(shí)處理多個(gè)任務(wù)的能力拄查。
4 線程狀態(tài)及其切換
下面的這個(gè)圖非常重要吁津!你如果看懂了這個(gè)圖,那么對(duì)于多線程的理解將會(huì)更加深刻堕扶。
- 新建狀態(tài)(New):新創(chuàng)建了一個(gè)線程對(duì)象碍脏。
- 就緒狀態(tài)(Runnable):線程對(duì)象創(chuàng)建后,
其他線程調(diào)用了該對(duì)象的start()方法稍算。該狀態(tài)的線程位于可運(yùn)行線程隊(duì)列中潮酒,變得可運(yùn)行,等待獲取CPU的使用權(quán)邪蛔。
- 運(yùn)行狀態(tài)(Running):就緒狀態(tài)的線程獲取了CPU急黎,執(zhí)行程序代碼。
- 阻塞狀態(tài)(Blocked):
阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán)侧到,暫時(shí)停止運(yùn)行勃教。直到線程進(jìn)入就緒狀態(tài),才有機(jī)會(huì)再次轉(zhuǎn)到運(yùn)行狀態(tài)匠抗。
阻塞的情況分三種:
等待阻塞:運(yùn)行的線程執(zhí)行wait()方法故源,JVM會(huì)把該線程放入等待池中(wait會(huì)釋放持有的鎖)
。
同步阻塞:運(yùn)行的線程在獲取對(duì)象的同步鎖時(shí)汞贸,若該同步鎖被別的線程占用绳军,則JVM會(huì)把該線程放入鎖池中
印机。
其他阻塞:運(yùn)行的線程執(zhí)行sleep()或join()方法,或者發(fā)出了I/O請(qǐng)求時(shí)门驾,JVM會(huì)把該線程置為阻塞狀態(tài)射赛。當(dāng)sleep()狀態(tài)超時(shí)它改、join()等待線程終止或者超時(shí)呼股、或者I/O處理完畢時(shí)陈轿,線程重新轉(zhuǎn)入就緒狀態(tài)(注意岸军,sleep不會(huì)釋放線程所持有的鎖)
。
- 死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法怀泊,該線程結(jié)束生命周期本砰。
Java線程在運(yùn)行的生命周期中可能處于下圖表中所示的6種不同的狀態(tài)蜒蕾, 在給定的一個(gè)時(shí)刻及汉,線程只能處于其中的一個(gè)狀態(tài)沮趣。
下面使用jstack工具(可以選擇打開終端, 鍵入jstack或者到JDK安裝目錄的bin目錄下執(zhí)行命令)坷随, 嘗試查看示例代碼運(yùn)行時(shí)的線程信息房铭, 更加深入地理解線程狀態(tài)。測試代碼如下:
public class ThreadState {
// 該線程不斷地進(jìn)行睡眠
static class TimeWaiting implements Runnable {
@Override
public void run() {
while (true) {
SleepUtils.second(100);
}
}
}
// 該線程在Waiting.class實(shí)例上等待
static class Waiting implements Runnable {
@Override
public void run() {
while (true) {
synchronized (Waiting.class) {
try {
Waiting.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
// 該線程在Blocked.class實(shí)例上加鎖后甸箱, 不會(huì)釋放該鎖
static class Blocked implements Runnable {
@Override
public void run() {
synchronized (Blocked.class) {
while (true) {
SleepUtils.second(100);
}
}
}
}
public static void main(String[] args) {
new Thread(new TimeWaiting (), "TimeWaitingThread").start();
new Thread(new Waiting(), "WaitingThread").start();
// 使用兩個(gè)Blocked線程, 一個(gè)獲取鎖成功迅脐, 另一個(gè)被阻塞
new Thread(new Blocked(), "BlockedThread-1").start();
new Thread(new Blocked(), "BlockedThread-2").start();
}
}
上述示例中使用的SleepUtils代碼如下:
public class SleepUtils {
public static final void second(long seconds) {
try {
TimeUnit.SECONDS.sleep(seconds) ;
} catch (InterruptedException e) {
}
}
}
運(yùn)行該示例芍殖, 打開終端或者命令提示符, 鍵入“jps”谴蔑, 輸出如下:
16544 Jps
13700
10156 ThreadState
可以看到運(yùn)行示例對(duì)應(yīng)的進(jìn)程ID是10156豌骏,接著再輸入“jstack 10156”,部分輸出如下:
//BlockedThread-2線程阻塞在獲取Blocked.class示例的鎖上
"BlockedThread-2" #13 prio=5 os_prio=0 tid=0x00000000180ad800 nid=0x108c waiting for monitor entry [0x0000000018e8f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at ThreadState$Blocked.run(ThreadState.java:35)
- waiting to lock <0x00000000e01e4db0> (a java.lang.Class for ThreadState$Blocked)
at java.lang.Thread.run(Thread.java:745)
//BlockedThread-1線程獲取到了Blocked.class的鎖隐锭,處于睡眠狀態(tài)
"BlockedThread-1" #12 prio=5 os_prio=0 tid=0x00000000180ad000 nid=0x2740 waiting on condition [0x0000000018d8e000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at SleepUtils.second(SleepUtils.java:6)
at ThreadState$Blocked.run(ThreadState.java:35)
- locked <0x00000000e01e4db0> (a java.lang.Class for ThreadState$Blocked)
at java.lang.Thread.run(Thread.java:745)
//WaitingThread線程在Waitting實(shí)例上等待
"WaitingThread" #11 prio=5 os_prio=0 tid=0x00000000180a6000 nid=0x93c in Object.wait() [0x0000000018c8f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000e01e37c0> (a java.lang.Class for ThreadState$Waiting)
at java.lang.Object.wait(Object.java:502)
at ThreadState$Waiting.run(ThreadState.java:21)
- locked <0x00000000e01e37c0> (a java.lang.Class for ThreadState$Waiting)
at java.lang.Thread.run(Thread.java:745)
//TimeWaitingThread線程處于超時(shí)等待
"TimeWaitingThread" #10 prio=5 os_prio=0 tid=0x00000000180a5000 nid=0x8c4 waiting on condition [0x0000000018b8f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at SleepUtils.second(SleepUtils.java:6)
at ThreadState$TimeWaiting.run(ThreadState.java:8)
at java.lang.Thread.run(Thread.java:745)
通過示例窃躲, 我們了解到Java程序運(yùn)行中線程狀態(tài)的具體含義。 線程在自身的生命周期中钦睡,并不是固定地處于某個(gè)狀態(tài)蒂窒, 而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進(jìn)行切換。
下面是一張更詳細(xì)的線程狀態(tài)遷移圖:
從圖中可以看到荞怒, 線程創(chuàng)建之后洒琢, 調(diào)用start()方法開始運(yùn)行(這里的運(yùn)行狀態(tài)其實(shí)是就緒態(tài)和運(yùn)行態(tài)的合集)。 當(dāng)線程執(zhí)行wait()方法之后褐桌, 線程進(jìn)入等待狀態(tài)衰抑。 進(jìn)入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運(yùn)行狀態(tài), 而超時(shí)等待狀態(tài)相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制荧嵌, 也就是超時(shí)時(shí)間到達(dá)時(shí)將會(huì)返回到運(yùn)行狀態(tài)呛踊。 當(dāng)線程調(diào)用同步方法時(shí)砾淌, 在沒有獲取到鎖的情況下, 線程將會(huì)進(jìn)入到阻塞狀態(tài)谭网。
線程在執(zhí)行Runnable的run()方法之后將會(huì)進(jìn)入到終止?fàn)顟B(tài)汪厨。
注意:阻塞狀態(tài)是線程在進(jìn)入synchronized關(guān)鍵字修飾的方法或代碼塊(嘗試獲取鎖) 時(shí)沒有拿到鎖的狀態(tài),但是阻塞在java.concurrent包中Lock接口 的線程狀態(tài)卻是等待狀態(tài)蜻底, 因?yàn)閖ava.concurrent包中 Lock接口對(duì)于阻塞的實(shí)現(xiàn)均使用了LockSupport類中的相關(guān)方法骄崩。
5 線程的啟動(dòng)、中斷與終止
線程對(duì)象在初始化完成之后薄辅, 調(diào)用start()方法就可以啟動(dòng)這個(gè)線程要拂。 線程start()方法的含義是: 當(dāng)前線程(即parent線程) 同步告知Java虛擬機(jī), 只要線程規(guī)劃器空閑站楚, 應(yīng)立即啟動(dòng)調(diào)用start()方法的線程脱惰。
注意:啟動(dòng)一個(gè)線程前, 最好為這個(gè)線程設(shè)置線程名 稱窿春, 因?yàn)檫@樣在使用jstack分析程序或者進(jìn)行問題排查時(shí)拉一, 就會(huì)給開發(fā)人員提供一些提示, 自定義的線程最好能夠起個(gè)名字旧乞。
中斷可以理解為線程的一個(gè)標(biāo)識(shí)位屬性蔚润, 它表示一個(gè)運(yùn)行中的線程是否被其他線程進(jìn)行了中斷操作。 中斷好比其他線程對(duì)該線程打了個(gè)招呼尺栖, 其他線程通過調(diào)用該線程的interrupt()方法對(duì)其進(jìn)行中斷操作嫡纠。
線程通過檢查自身是否被中斷來進(jìn)行響應(yīng), 線程通過方法isInterrupted()來進(jìn)行判斷是否被中斷延赌, 也可以調(diào)用靜態(tài)方法Thread.interrupted()對(duì)當(dāng)前線程的中斷標(biāo)識(shí)位進(jìn)行復(fù)位除盏。
如果該線程已經(jīng)處于終結(jié)
狀態(tài), 即使該線程被中斷過挫以, 在調(diào)用該線程對(duì)象的isInterrupted()時(shí)依舊會(huì)返回false者蠕。
調(diào)用某個(gè)線程的interrupt()方法,將會(huì)設(shè)置該線程為中斷狀態(tài)掐松,即設(shè)置為true踱侣。線程中斷后的結(jié)果是死亡、還是等待新的任務(wù)或是繼續(xù)運(yùn)行至下一步大磺,取決于這個(gè)程序本身泻仙。線程可以不時(shí)地檢測這個(gè)中斷標(biāo)識(shí)位,以判斷線程是否應(yīng)該被中斷(中斷標(biāo)志是否為true)量没。它并不像stop方法那樣會(huì)真的會(huì)粗暴地打斷一個(gè)正在運(yùn)行的線程玉转。
從Java的API中可以看到, 有許多聲明拋出InterruptedException的方法(例如Thread.sleep(long millis)方法)殴蹄, 這些方法在拋出InterruptedException之前究抓, Java虛擬機(jī)會(huì)先將該線程的中斷標(biāo)識(shí)位清除猾担, 然后拋出InterruptedException, 此時(shí)調(diào)用isInterrupted()方法將會(huì)返回false刺下。
測試代碼如下绑嘹,首先創(chuàng)建了兩個(gè)線程, SleepThread和BusyThread橘茉, 前者不停地睡眠工腋, 后者一直運(yùn)行, 然后對(duì)這兩個(gè)線程分別進(jìn)行中斷操作畅卓, 觀察二者的中斷標(biāo)識(shí)位擅腰。
public class Interrupted {
public static void main(String[ ] args) throws Exception {
// sleepThread不停的嘗試睡眠
Thread sleepThread = new Thread(new SleepRunner() , "SleepThread");
sleepThread.setDaemon(true);
// busyThread不停的運(yùn)行
Thread busyThread = new Thread(new BusyRunner() , "BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
// 休眠5秒, 讓sleepThread和busyThread充分運(yùn)行
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
// 防止sleepThread和busyThread立刻退出
SleepUtils. second(2);
}
static class SleepRunner implements Runnable {
@Override
public void run() {
while (true) {
SleepUtils. second(10) ;
}
}
}
static class BusyRunner implements Runnable {
@Override
public void run() {
while (true) {
}
}
}
}
輸出如下:
SleepThread interrupted is false
BusyThread interrupted is true
從結(jié)果可以看出翁潘, 拋出InterruptedException的線程SleepThread趁冈, 其中斷標(biāo)識(shí)位被清除了,而一直忙碌運(yùn)作的線程BusyThread拜马, 中斷標(biāo)識(shí)位沒有被清除渗勘。
中斷狀態(tài)是線程的一個(gè)標(biāo)識(shí)位, 而中斷操作是一種簡便的線程間交互方式俩莽, 而這種交互方式最適合用來取消或停止任務(wù)旺坠。 除了中斷以外, 還可以利用一個(gè)boolean共享變量來控制是否需要停止任務(wù)并終止該線程扮超,這是最受推薦的終止一個(gè)線程(就是讓一個(gè)線程徹底停止運(yùn)行)的方式取刃,使用共享變量(shared variable)來發(fā)出信號(hào),告訴線程必須停止正在運(yùn)行的任務(wù)瞒津。線程必須周期性的核查這一變量蝉衣,然后有秩序地停止任務(wù)括尸。
測試代碼如下:
public class Shutdown {
public static void main(String[ ] args) throws Exception {
Runner one = new Runner() ;
Thread countThread = new Thread(one, "CountThread") ;
countThread.start() ;
// 睡眠1秒巷蚪,main線程對(duì)Runner one進(jìn)行中斷, 使CountThread能夠感知中斷標(biāo)識(shí)位的置位而結(jié)束
TimeUnit.SECONDS.sleep(1) ;
countThread.interrupt() ;
Runner two = new Runner() ;
countThread = new Thread(two, "CountThread") ;
countThread.start() ;
// 睡眠1秒濒翻,main線程對(duì)Runner two進(jìn)行取消屁柏, 使CountThread能夠感知on為false而結(jié)束
TimeUnit.SECONDS.sleep(1) ;
two.cancel() ;
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && ! Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}
輸出結(jié)果如下(多次運(yùn)行結(jié)果可能不同):
Count i = 543487324
Count i = 540898082
示例在執(zhí)行過程中, main線程通過中斷操作和cancel()方法均可使CountThread得以終止有送。這種通過標(biāo)識(shí)位或者中斷操作的方式能夠使線程在終止時(shí)有機(jī)會(huì)去清理資源淌喻, 而不是武斷地將線程停止
, 因此這種終止線程的做法顯得更加安全和優(yōu)雅雀摘。
注意:suspend()裸删、resume()和stop()方法也可以完成線程的暫停、恢復(fù)和終止工作阵赠,而且非逞乃“人性化”肌稻。但是這些API是過期的,也就是不建議使用的匕荸。
不建議使用的原因主要有:以suspend()方法為例爹谭,在調(diào)用后,線程不會(huì)釋放已經(jīng)占有的資源(比如鎖)榛搔,而是占有著資源進(jìn)入睡眠狀態(tài)诺凡,這樣容易引發(fā)死鎖問題。同樣践惑,stop()方法在終結(jié)一個(gè)線程時(shí)不會(huì)保證線程的資源正常釋放腹泌,通常是沒有給予線程完成資源釋放工作的機(jī)會(huì),因此會(huì)導(dǎo)致程序可能工作在不確定狀態(tài)下童本。
正因?yàn)閟uspend()真屯、resume()和stop()方法帶來的副作用,這些方法才被標(biāo)注為不建議使用的過期方法穷娱,而暫停和恢復(fù)操作可以用后面提到的等待/通知機(jī)制
來替代绑蔫。