作者:追夢
前言
借用 Java 并發編程實踐中的話:編寫正確的程序并不容易,而編寫正常的并發程序就更難了;相比于順序執行的情況,多線程的線程安全問題是微妙而且出乎意料的,因為在沒有進行適當同步的情況下多線程中各個操作的順序是不可預期的。
并發編程相比 Java 中其他知識點學習起來門檻相對較高,學習起來比較費勁,從而導致很多人望而卻步;而無論是職場面試和高并發高流量的系統的實現卻都還離不開并發編程,從而導致能夠真正掌握并發編程的人才成為市場比較迫切需求的。
本文作為 Java 并發編程之美系列的開篇,首先通過通俗易懂的方式先來和大家聊聊多線程并發編程線程有關基礎知識(本文結合示例進行講解,定會讓你耳目一新),具體內容如下:
- 什么是線程?線程和進程的關系。
- 線程創建與運行。創建一個線程有那幾種方式?有何區別?
- 線程通知與等待,多線程同步的基礎設施。
- 線程的虛假喚醒,以及如何避免。
- 等待線程執行終止的 方法。想讓主線程在子線程執行完畢后在做一點事情?
- 讓線程睡眠的 sleep 方法,sleep 的線程會釋放持有的鎖?
- 線程中斷。中斷一個線程,被中斷的線程會自己終止?
- 理解線程上下文切換。線程多了一定好?
- 線程死鎖,以及如何避免。
- 守護線程與用戶線程。當 main 函數執行完畢,但是還有用戶線程存在的時候,JVM 進程會退出?
什么是線程
在討論什么是線程前有必要先說下什么是進程,因為線程是進程中的一個實體,線程本身是不會獨立存在的。進程是代碼在數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,線程則是進程的一個執行路徑,一個進程至少有一個線程,進程中的多個線程是共享進程的資源的。
操作系統在分配資源時候是把資源分配給進程的,但是 CPU 資源就比較特殊,它是分派到線程的,因為真正要占用 CPU 運行的是線程,所以也說線程是 CPU 分配的基本單位。
Java 中當我們啟動 main 函數時候其實就啟動了一個 JVM 的進程,而 main 函數所在線程就是這個進程中的一個線程,也叫做主線程。
如圖一個進程中有多個線程,多個線程共享進程的堆和方法區資源,但是每個線程有自己的程序計數器,棧區域。
其中程序計數器是一塊內存區域,用來記錄線程當前要執行的指令地址,那么程序計數器為何要設計為線程私有的呢?前面說了線程是占用 CPU 執行的基本單位,而 CPU 一般是使用時間片輪轉方式讓線程輪詢占用的,所以當前線程 CPU 時間片用完后,要讓出 CPU,等下次輪到自己時候在執行,那么如何知道之前程序執行到哪里了?其實程序計數器就是為了記錄該線程讓出 CPU 時候的執行地址,待再次分配到時間片時候就可以從自己私有的計數器指定地址繼續執行了。
另外每個線程有自己的棧資源,用于存儲該線程的局部變量,這些局部變量是該線程私有的,其它線程是訪問不了的,另外棧還用來存放線程的調用棧幀。
堆是一個進程中最大的一塊內存,堆是被進程中的所有線程共享的,是進程創建時候分配的,堆里面主要存放使用 new 操作創建的對象實例。
方法區則是用來存放進程中的代碼片段的,是線程共享的。
線程創建與運行
Java 中有三種線程創建方法,分別為實現 Runnable 接口的run方法、繼承 Thread 類并重寫 run 方法、使用 FutureTask 方式。
首先看下繼承 Thread 方法的實現:
public class ThreadTest {
//繼承Thread類并重寫run方法
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("I am a child thread");
}
}
public static void main(String[] args) {
// 創建線程
MyThread thread = new MyThread();
// 啟動線程
thread.start();
}
}
如上代碼 MyThread 類繼承了 Thread 類,并重寫了 run 方法,然后調用了線程的 start 方法啟動了線程,當創建完 thread 對象后該線程并沒有被啟動執行.
當調用了 start 方法后才是真正啟動了線程。其實當調用了 start 方法后線程并沒有馬上執行而是處于就緒狀態,這個就緒狀態是指該線程已經獲取了除 CPU 資源外的其它資源,等獲取 CPU 資源后才會真正處于運行狀態。
當 run 方法執行完畢,該線程就處于終止狀態了。使用繼承方式好處是 run 方法內獲取當前線程直接使用 this 就可以,無須使用 Thread.currentThread() 方法,不好的地方是 Java 不支持多繼承,如果繼承了 Thread 類那么就不能再繼承其它類,另外任務與代碼沒有分離,當多個線程執行一樣的任務時候需要多份任務代碼,而 Runable 則沒有這個限制,下面看下實現 Runnable 接口的 run 方法方式:
public static class RunableTask implements Runnable{
@Override
public void run() {
System.out.println("I am a child thread");
}
}
public static void main(String[] args) throws InterruptedException{
RunableTask task = new RunableTask();
new Thread(task).start();
new Thread(task).start();
}
如上面代碼,兩個線程公用一個 task 代碼邏輯,需要的話 RunableTask 可以添加參數進行任務區分,另外 RunableTask 可以繼承其他類,但是上面兩種方法都有一個缺點就是任務沒有返回值,下面看最后一種是使用 FutureTask:
//創任務類,類似Runable
public static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception {
return "hello";
}
}
public static void main(String[] args) throws InterruptedException {
// 創建異步任務
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
//啟動線程
new Thread(futureTask).start();
try {
//等待任務執行完畢,并返回結果
String result = futureTask.get();
System.out.println(result);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
注:每種方式都有自己的優缺點,應該根據實際場景進行選擇。
線程通知與等待
Java 中 Object 類是所有類的父類,鑒于繼承機制,Java 把所有類都需要的方法放到了 Object 類里面,其中就包含本節要講的通知等待系列函數,這些通知等待函數是組成并發包中線程同步組件的基礎。
下面講解下 Object 中關于線程同步的通知等待函數。
void wait() 方法
首先談下什么是共享資源,所謂共享資源是說該資源被多個線程共享,多個線程都可以去訪問或者修改的資源。另外本文當講到的共享對象就是共享資源。
當一個線程調用一個共享對象的 wait() 方法時候,調用線程會被阻塞掛起,直到下面幾個事情之一發生才返回:
- 其它線程調用了該共享對象的 notify() 或者 notifyAll() 方法;
- 其它線程調用了該線程的 interrupt() 方法設置了該線程的中斷標志,該線程會拋出 InterruptedException 異常返回。
另外需要注意的是如果調用 wait() 方法的線程沒有事先獲取到該對象的監視器鎖,則調用 wait() 方法時候調用線程會拋出 IllegalMonitorStateException 異常。
那么一個線程如何獲取到一個共享變量的監視器那?
(1)執行使用 synchronized 同步代碼塊時候,使用該共享變量作為參數:
synchronized(共享變量){
//doSomething
}
(2)調用該共享變量的方法,并且該方法使用了 synchronized 修飾:
synchronized void add(int a,int b){
//doSomething
}
另外需要注意的是一個線程可以從掛起狀態變為可以運行狀態(也就是被喚醒)即使該線程沒有被其它線程調用 notify(),notifyAll() 進行通知,或者被中斷,或者等待超時,這就是所謂的虛假喚醒。
雖然虛假喚醒在應用實踐中很少發生,但是還是需要防范于未然的,做法就是不停的去測試該線程被喚醒的條件是否滿足,不滿足則繼續等待,也就是說在一個循環中去調用 wait() 方法進行防范,退出循環的條件是條件滿足了喚醒該線程。
synchronized (obj) {
while (條件不滿足){
obj.wait();
}
}
如上代碼是經典的調用共享變量 wait() 方法的實例,首先通過同步塊獲取 obj 上面的監視器鎖,然后通過 while 循環內調用 obj 的 wait() 方法。
下面從生產者消費者例子來加深理解,如下面代碼是一個生產者的例子,其中 queue 為共享變量,生產者線程在調用 queue 的 wait 方法前,通過使用 synchronized 關鍵字拿到了該共享變量 queue 的監視器,所以調用 wait() 方法才不會拋出 IllegalMonitorStateException 異常,如果當前隊列沒有空閑容量則會調用 queued 的 wait() 掛起當前線程,這里使用循環就是為了避免上面說的虛假喚醒問題,這里假如當前線程虛假喚醒了,但是隊列還是沒有空余容量的話,當前線程還是會調用 wait() 把自己掛起。
//生產線程
synchronized (queue) {
//消費隊列滿,則等待隊列空閑
while (queue.size() == MAX_SIZE) {
try {
//掛起當前線程,并釋放通過同步塊獲取的queue上面的鎖,讓消費線程可以獲取該鎖,然后獲取隊列里面元素
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
//空閑則生成元素,并通知消費線程
queue.add(ele);
queue.notifyAll();
}
}
//消費線程
synchronized (queue) {
//消費隊列為空
while (queue.size() == 0) {
try
//掛起當前線程,并釋放通過同步塊獲取的queue上面的鎖,讓生產線程可以獲取該鎖,生產元素放入隊列
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
//消費元素,并通知喚醒生產線程
queue.take();
queue.notifyAll();
}
}
另外當一個線程調用了共享變量的 wait() 方法后該線程會被掛起,同時該線程會暫時釋放對該共享變量監視器的持有,直到另外一個線程調用了共享變量的 notify() 或者 notifyAll() 方法才有可能會重新獲取到該共享變量的監視器的持有權(這里說有可能,是因為考慮到多個線程第一次都調用了 wait() 方法,所以多個線程會競爭持有該共享變量的監視器)。
借用上面這個例子來講解下調用共享變量 wait() 方法后當前線程會釋放持有的共享變量的鎖的理解。
如上代碼假如生產線程 A 首先通過 synchronized 獲取到了 queue 上的鎖,那么其它生產線程和所有消費線程都會被阻塞,線程 A 獲取鎖后發現當前隊列已滿會調用 queue.wait() 方法阻塞自己,然后會釋放獲取的 queue 上面的鎖,這里考慮下為何要釋放該鎖?如果不釋放,由于其它生產線程和所有消費線程已經被阻塞掛起,而線程 A 也被掛起,這就處于了死鎖狀態。這里線程 A 掛起自己后釋放共享變量上面的鎖就是為了打破死鎖必要條件之一的持有并等待原則。關于死鎖下面章節會有講到,線程 A 釋放鎖后其它生產線程和所有消費線程中會有一個線程獲取 queue 上的鎖進而進入同步塊,這就打破了死鎖。
最后再舉一個例子說明當一個線程調用共享對象的 wait() 方法被阻塞掛起后,如果其它線程中斷了該線程,則該線程會拋出 InterruptedException 異常后返回:
public class WaitNotifyInterupt {
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
//創建線程
Thread threadA = new Thread(new Runnable() {
public void run() {
try {
System.out.println("---begin---");
//阻塞當前線程
synchronized (obj) {
obj.wait();
}
System.out.println("---end---");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
Thread.sleep(1000);
System.out.println("---begin interrupt threadA---");
threadA.interrupt();
System.out.println("---end interrupt threadA---");
}
}
運行上面代碼輸出為:
如上代碼 threadA 調用了共享對 obj 的 wait() 方法后阻塞掛起了自己,然后主線程在休眠1s后中斷了 threadA 線程,可知中斷后 threadA 在 obj.wait() 處拋出了 java.lang.IllegalMonitorStateException 異常后返回后終止。
void wait(long timeout) 方法
該方法相比 wait() 方法多一個超時參數,不同在于如果一個線程調用了共享對象的該方法掛起后,如果沒有在指定的 timeout ms 時間內被其它線程調用該共享變量的 notify() 或者 notifyAll() 方法喚醒,那么該函數還是會因為超時而返回。
需要注意的是如果在調用該函數時候 timeout 傳遞了負數會拋出 IllegalArgumentException 異常。
void wait(long timeout, int nanos) 方法
內部是調用 wait(long timeout),如下代碼:只是當 nanos>0 時候讓參數一遞增1。
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
void notify() 方法
一個線程調用共享對象的 notify() 方法后,會喚醒一個在該共享變量上調用 wait 系列方法后被掛起的線程,一個共享變量上可能會有多個線程在等待,具體喚醒哪一個等待的線程是隨機的。
另外被喚醒的線程不能馬上從 wait 返回繼續執行,它必須獲取了共享對象的監視器后才可以返回,也就是喚醒它的線程釋放了共享變量上面的監視器鎖后,被喚醒它的線程也不一定會獲取到共享對象的監視器,這是因為該線程還需要和其它線程一塊競爭該鎖,只有該線程競爭到了該共享變量的監視器后才可以繼續執行。
類似 wait 系列方法,只有當前線程已經獲取到了該共享變量的監視器鎖后,才可以調用該共享變量的 notify() 方法,否者會拋出 IllegalMonitorStateException 異常。
void notifyAll() 方法
不同于 nofity() 方法在共享變量上調用一次就會喚醒在該共享變量上調用 wait 系列方法被掛起的一個線程,notifyAll() 則會喚醒所有在該共享變量上由于調用 wait 系列方法而被掛起的線程。
最后本小節最后講一個例子來說明 notify() 和 notifyAll() 的具體含義和一些需要注意的地方,代碼實例如下:
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
// 創建線程
Thread threadA = new Thread(new Runnable() {
public void run() {
// 獲取resourceA共享資源的監視器鎖
synchronized (resourceA) {
System.out.println("threadA get resourceA lock");
try {
System.out.println("threadA begin wait");
resourceA.wait();
System.out.println("threadA end wait");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
// 創建線程
Thread threadB = new Thread(new Runnable() {
public void run() {
synchronized (resourceA) {
System.out.println("threadB get resourceA lock");
try {
System.out.println("threadB begin wait");
resourceA.wait();
System.out.println("threadB end wait");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
// 創建線程
Thread threadC = new Thread(new Runnable() {
public void run() {
synchronized (resourceA) {
System.out.println("threadC begin notify");
resourceA.notifyAll();
}
}
});
// 啟動線程
threadA.start();
threadB.start();
Thread.sleep(1000);
threadC.start();
// 等待線程結束
threadA.();
threadB.();
threadC.();
System.out.println("main over");
}
輸出結果:
如上代碼開啟了三個線程,其中線程 A 和 B 分別調用了共享資源 resourceA 的 wait() 方法,線程 C 則調用了 nofity() 方法。
這里啟動線程 C 前首先調用 sleep 方法讓主線程休眠 1s,目的是讓線程 A 和 B 全部執行到調用 wait 方法后在調用線程 C 的 notify 方法。
這個例子企圖希望在線程 A 和線程 B 都因調用共享資源 resourceA 的 wait() 方法而被阻塞后,線程 C 在調用 resourceA 的 notify() 方法,希望可以喚醒線程 A 和線程 B,但是從執行結果看只有一個線程 A 被喚醒了,線程 B 沒有被喚醒,
從結果看線程調度器這次先調度了線程 A 占用 CPU 來運行,線程 A 首先獲取 resourceA 上面的鎖,然后調用 resourceA 的 wait() 方法掛起當前線程并釋放獲取到的鎖,然后線程 B 獲取到 resourceA 上面的鎖并調用了 resourceA 的 wait(),此時線程 B 也被阻塞掛起并釋放了 resourceA 上的鎖。
線程 C 休眠結束后在共享資源 resourceA 上調用了 notify() 方法,則會激活 resourceA 的阻塞集合里面的一個線程,這里激活了線程 A,所以線程 A 調用的 wait() 方法返回了,線程 A 執行完畢。而線程 B 還處于阻塞狀態。
如果把線程 C 里面調用的 notify() 改為調用 notifyAll() 而執行結果如下:
可知線程 A 和線程 B 被掛起后,線程 C 調用 notifyAll() 函數會喚醒在 resourceA 等待的所有線程,這里線程 A 和線程 B 都會被喚醒,只是線程 B 先獲取到 resourceA 上面的鎖然后從 wait() 方法返回,等線程 B 執行完畢后,線程 A 又獲取了 resourceA 上面的鎖,然后從 wait() 方返回,當線程 A 執行完畢,主線程就返回后,然后打印輸出。
注:在調用具體共享對象的 wait 或者 notify 系列函數前要先獲取共享對象的鎖;另外通知和等待是實現線程同步的原生方法,理解它們的協作功能很有必要;最后由于線程虛假喚醒的存在,一定要使用循環檢查的方式。
等待線程執行終止的 join 方法
在項目實踐時候經常會遇到一個場景,就是需要等待某幾件事情完成后才能繼續往下執行,比如多個線程去加載資源,當多個線程全部加載完畢后在匯總處理,Thread 類中有個 join 方法就可以做這個事情,前面介紹的等待通知方法是屬于 Object 類的,而 join 方法則是直接在 Thread 類里面提供的,join 是無參,返回值為 void 的方法。下面看一個簡單的例子來介紹 join 的使用:
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("child threadOne over!");
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("child threadTwo over!");
}
});
//啟動子線程
threadOne.start();
threadTwo.start();
System.out.println("wait all child thread over!");
//等待子線程執行完畢,返回
threadOne.join();
threadTwo.join();
System.out.println("all child thread over!");
}
如代碼主線程里面啟動了兩個子線程,然后在分別調用了它們的 join() 方法,那么主線程首先會阻塞到 threadOne.join() 方法,等 threadOne 執行完畢后返回,threadOne 執行完畢后 threadOne.join() 就會返回,然后主線程調用 threadTwo.join() 后再次被阻塞,等 threadTwo 執行完畢后主線程也就返回了。這里只是為了演示 join 的作用,對應這類需求后面會講的 CountDownLatch 是不錯選擇。
另外線程 A 調用線程 B 的 join 方法后會被阻塞,當其它線程調用了線程 B 的 interrupt() 方法中斷了線程 B 時候,線程 B 會拋出 InterruptedException 異常而返回,下面通過一個例子來加深理解:
public static void main(String[] args) throws InterruptedException {
//線程one
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("threadOne begin run!");
for (;;) {
}
}
});
//獲取主線程
final Thread mainThread = Thread.currentThread();
//線程two
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
//休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中斷主線程
mainThread.interrupt();
}
});
// 啟動子線程
threadOne.start();
//延遲1s啟動線程
threadTwo.start();
try{//等待線程one執行結束
threadOne.join();
}catch(InterruptedException e){
System.out.println("main thread:" + e);
}
}
輸出結果:
如上代碼 threadOne 線程里面執行死循環,主線程調用 threadOne 的 join 方法阻塞自己等待線程 threadOne 執行完畢,待 threadTwo 休眠 1s 后會調用主線程的 interrupt() 方法設置主線程的中斷標志。
從結果看主線程中 threadOne.join() 處會拋出 InterruptedException 異常而返回。這里需要注意的是 threadTwo 里面調用的是主線程的 interrupt(),而不是線程 threadOne 的。
注:由于 CountDownLatch 功能比 join 更豐富,所以項目實踐中一般使用 CountDownLatch,關于 CountDownLatch,后面 Chat《Java 并發編程之美高級篇》會有具體講解。
讓線程睡眠的 sleep 方法
Thread 類中有一個靜態的 sleep 方法,當一個執行中的線程調用了 Thread 的 sleep 方法后,調用線程會暫時讓出指定時間的執行權,也就是這期間不參與 CPU 的調度,但是該線程所擁有的監視器資源,比如鎖還是持有不讓出的。當指定的睡眠時間到了該函數會正常返回,線程就處于就緒狀態,然后參與 CPU 的調度,當獲取到了 CPU 資源就可以繼續運行了。如果在睡眠期間其它線程調用了該線程的 interrupt() 方法中斷了該線程,該線程會在調用 sleep 的地方拋出 InterruptedException 異常返回。
首先看一個例子來說明線程在睡眠時候擁有的監視器資源不會被釋放是什么意思:
public class SleepTest2 {
// 創建一個獨占鎖
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 創建線程A
Thread threadA = new Thread(new Runnable() {
public void run() {
// 獲取獨占鎖
lock.lock();
try {
System.out.println("child threadA is in sleep");
Thread.sleep(10000);
System.out.println("child threadA is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
}
}
});
// 創建線程B
Thread threadB = new Thread(new Runnable() {
public void run() {
// 獲取獨占鎖
lock.lock();
try {
System.out.println("child threadB is in sleep");
Thread.sleep(10000);
System.out.println("child threadB is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
}
}
});
// 啟動線程
threadA.start();
threadB.start();
}
}
執行結果:
如上代碼首先創建了一個獨占鎖,然后創建了兩個線程,每個線程內部先獲取鎖,然后睡眠,睡眠結束后會釋放鎖。
首先無論你執行多少遍上面的代碼都是先輸出線程 A 的打印或者先輸出線程 B 的打印,不會存在線程 A 和線程 B 交叉打印的情況。
從執行結果看線程 A 先獲取了鎖,那么線程 A 會先打印一行,然后調用 sleep 讓自己沉睡 10s,在線程 A 沉睡的這 10s 內那個獨占鎖 lock 還是線程 A 自己持有的,線程 B 會一直阻塞直到線程 A 醒過來后執行 unlock 釋放鎖。
下面在來看下當一個線程處于睡眠時候如果另外一個線程中斷了它,會不會在調用 sleep 處拋出異常。
public static void main(String[] args) throws InterruptedException {
//創建線程
Thread thread = new Thread(new Runnable() {
public void run() {
try {
System.out.println("child thread is in sleep");
Thread.sleep(10000);
System.out.println("child thread is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//啟動線程
thread.start();
//主線程休眠2s
Thread.sleep(2000);
//主線程中斷子線程
thread.interrupt();
}
執行結果:
如上代碼在子線程睡眠期間主線程中斷了它,所以子線程在調用 sleep 處拋出了 InterruptedException 異常。
注: sleep 方法只是會讓調用線程暫時讓出指定時間的 CPU 執行權,但是該線程所擁有的監視器資源,比如鎖還是持有不讓出的。
線程中斷
Java 中線程中斷是一種線程間協作模式,通過設置線程的中斷標志并不能直接終止該線程的執行,而是需要被中斷的線程根據中斷狀態自行處理。
- void interrupt() 方法
中斷線程,例如當線程 A 運行時,線程 B 可以調用線程 A 的 interrupt() 方法來設置線程 A 的中斷標志為 true 并立即返回。設置標志僅僅是設置標志,線程 A 并沒有實際被中斷,會繼續往下執行的。如果線程 A 因為調用了 wait 系列函數或者 join 方法或者 sleep 函數而被阻塞掛起,這時候線程 B 調用了線程 A 的 interrupt() 方法,線程 A 會在調用這些方法的地方拋出 InterruptedException 異常而返回。
- boolean isInterrupted()
檢測當前線程是否被中斷,如果是返回 true,否者返回 false。
public boolean isInterrupted() {
//傳遞false,說明不清除中斷標志
return isInterrupted(false);
}
- boolean interrupted()
檢測當前線程是否被中斷,如果是返回 true,否者返回 false,與 isInterrupted 不同的是該方法如果發現當前線程被中斷后會清除中斷標志,并且該函數是 static 方法,可以通過 Thread 類直接調用。另外從下面代碼可以知道 interrupted() 內部是獲取當前調用線程的中斷標志而不是調用 interrupted() 方法的實例對象的中斷標志。
public static boolean interrupted() {
//清除中斷標志
return currentThread().isInterrupted(true);
}
下面看一個線程使用 Interrupted 優雅退出的經典使用例子,代碼如下:
public void run(){
try{
....
//線程退出條件
while(!Thread.currentThread().isInterrupted()&& more work to do){
// do more work;
}
}catch(InterruptedException e){
// thread was interrupted during sleep or wait
}
finally{
// cleanup, if required
}
}
下面看一個根據中斷標志判斷線程是否終止的例子:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//如果當前線程被中斷則退出循環
while (!Thread.currentThread().isInterrupted())
System.out.println(Thread.currentThread() + " hello");
}
});
//啟動子線程
thread.start();
//主線程休眠1s,以便中斷前讓子線程輸出點東西
Thread.sleep(1);
//中斷子線程
System.out.println("main thread interrupt thread");
thread.interrupt();
//等待子線程執行完畢
thread.join();
System.out.println("main is over");
}
輸出結果:
如上代碼子線程 thread 通過檢查當前線程中斷標志來控制是否退出循環,主線程在休眠 1s 后調用 thread 的 interrupt() 方法設置了中斷標志,所以線程 thread 退出了循環。
注:中斷一個線程僅僅是設置了該線程的中斷標志,也就是設置了線程里面的一個變量的值,本身是不能終止當前線程運行的,一般程序里面是檢查這個標志的狀態來判斷是否需要終止當前線程。
理解線程上下文切換
在多線程編程中,線程個數一般都大于 CPU 個數,而每個 CPU 同一時刻只能被一個線程使用,為了讓用戶感覺多個線程是在同時執行,CPU 資源的分配采用了時間片輪轉的策略,也就是給每個線程分配一個時間片,在時間片內占用 CPU 執行任務。當前線程的時間片使用完畢后當前就會處于就緒狀態并讓出 CPU 讓其它線程占用,這就是上下文切換,從當前線程的上下文切換到了其它線程。
那么就有一個問題讓出 CPU 的線程等下次輪到自己占有 CPU 時候如何知道之前運行到哪里了?所以在切換線程上下文時候需要保存當前線程的執行現場,當再次執行時候根據保存的執行現場信息恢復執行現場。
線程上下文切換時機:
- 當前線程的 CPU 時間片使用完畢處于就緒狀態時候;
- 當前線程被其它線程中斷時候。
注:由于線程切換是有開銷的,所以并不是開的線程越多越好,比如如果機器是4核心的,你開啟了100個線程,那么同時執行的只有4個線程,這100個線程會來回切換線程上下文來共享這四個 CPU。
線程死鎖
什么是線程死鎖
死鎖是指兩個或兩個以上的線程在執行過程中,因爭奪資源而造成的互相等待的現象,在無外力作用的情況下,這些線程會一直相互等待而無法繼續運行下去。
如上圖,線程 A 已經持有了資源1的同時還想要資源2,線程 B 在持有資源2的時候還想要資源1,所以線程1和線程2就相互等待對方已經持有的資源,就進入了死鎖狀態。
那么產生死鎖的原因都有哪些,學過操作系統的應該都知道死鎖的產生必須具備以下四個必要條件。
- 互斥條件:指線程對已經獲取到的資源進行排它性使用,即該資源同時只由一個線程占用。如果此時還有其它進行請求獲取該資源,則請求者只能等待,直至占有資源的線程用畢釋放。
- 請求并持有條件:指一個線程已經持有了至少一個資源,但又提出了新的資源請求,而新資源已被其其它線程占有,所以當前線程會被阻塞,但阻塞的同時并不釋放自己已經獲取的資源。
- 不可剝奪條件:指線程獲取到的資源在自己使用完之前不能被其它線程搶占,只有在自己使用完畢后由自己釋放。
- 環路等待條件:指在發生死鎖時,必然存在一個線程——資源的環形鏈,即線程集合{T0,T1,T2,···,Tn}中的 T0 正在等待一個 T1 占用的資源;T1 正在等待 T2 占用的資源,……Tn正在等待已被 T0 占用的資源。
下面通過一個案例來說明線程死鎖:
public class DeadLockTest2 {
// 創建資源
private static Object resourceA = new Object();
private static Object resourceB = new Object();
public static void main(String[] args) {
// 創建線程A
Thread threadA = new Thread(new Runnable() {
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread() + " get ResourceA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get ResourceB");
synchronized (resourceB) {
System.out.println(Thread.currentThread() + "get ResourceB");
}
}
}
});
// 創建線程B
Thread threadB = new Thread(new Runnable() {
public void run() {
synchronized (resourceB) {
System.out.println(Thread.currentThread() + " get ResourceB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get ResourceA");
synchronized (resourceA) {
System.out.println(Thread.currentThread() + "get ResourceA");
}
};
}
});
// 啟動線程
threadA.start();
threadB.start();
}
}
輸出結果:
下面分析下代碼和結果,其中 Thread-0 是線程 A,Thread-1 是線程 B,代碼首先創建了兩個資源,并創建了兩個線程。
從輸出結果可以知道線程調度器先調度了線程 A,也就是把 CPU 資源讓給了線程 A,線程 A 調用了 getResourceA() 方法,方法里面使用 synchronized(resourceA) 方法獲取到了 resourceA 的監視器鎖,然后調用 sleep 函數休眠 1s,休眠 1s 是為了保證線程 A 在執行 getResourceB 方法前讓線程 B 搶占到 CPU 執行 getResourceB 方法。
線程 A 調用了 sleep 期間,線程 B 會執行 getResourceB 方法里面的 synchronized(resourceB),代表線程 B 獲取到了 objectB 對象的監視器鎖資源,然后調用 sleep 函數休眠 1S。
好了,到了這里線程 A 獲取到了 objectA 的資源,線程 B 獲取到了 objectB 的資源。線程 A 休眠結束后會調用 getResouceB 方法企圖獲取到 ojbectB 的資源,而 ObjectB 資源被線程 B 所持有,所以線程 A 會被阻塞而等待。而同時線程 B 休眠結束后會調用 getResourceA 方法企圖獲取到 objectA 上的資源,而資源 objectA 已經被線程 A 持有,所以線程 A 和 B 就陷入了相互等待的狀態也就產生了死鎖。
下面從產生死鎖的四個條件來談談本案例如何滿足了四個條件。
首先資源 resourceA 和 resourceB 都是互斥資源,當線程 A 調用 synchronized(resourceA) 獲取到 resourceA 上的監視器鎖后釋放前,線程 B 在調用 synchronized(resourceA) 嘗試獲取該資源會被阻塞,只有線程 A 主動釋放該鎖,線程 B 才能獲得,這滿足了資源互斥條件。
線程 A 首先通過 synchronized(resourceA) 獲取到 resourceA 上的監視器鎖資源,然后通過 synchronized(resourceB) 等待獲取到 resourceB 上的監視器鎖資源,這就構造了持有并等待。
線程 A 在獲取 resourceA 上的監視器鎖資源后,不會被線程 B 掠奪走,只有線程 A 自己主動釋放 resourceA 的資源時候,才會放棄對該資源的持有權,這構造了資源的不可剝奪條件。
線程 A 持有 objectA 資源并等待獲取 objectB 資源,而線程 B 持有 objectB 資源并等待 objectA 資源,這構成了循環等待條件。
所以線程 A 和 B 就形成了死鎖狀態。
如何避免線程死鎖
要想避免死鎖,需要破壞構造死鎖必要條件的至少一個即可,但是學過操作系統童鞋應該都知道目前只有持有并等待和循環等待是可以被破壞的。
造成死鎖的原因其實和申請資源的順序有很大關系,使用資源申請的有序性原則就可以避免死鎖,那么什么是資源的有序性呢,先看一下對上面代碼的修改:
// 創建線程B
Thread threadB = new Thread(new Runnable() {
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread() + " get ResourceB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get ResourceA");
synchronized (resourceB) {
System.out.println(Thread.currentThread() + "get ResourceA");
}
};
}
});
輸出結果:
如上代碼可知修改了線程 B 中獲取資源的順序和線程 A 中獲取資源順序一致,其實資源分配有序性就是指假如線程 A 和 B 都需要資源1,2,3……n 時候,對資源進行排序,線程 A 和 B 只有在獲取到資源 n-1 時候才能去獲取資源 n。
注:編寫并發程序,多個線程進行共享多個資源時候要注意采用資源有序分配法避免死鎖的產生。
守護線程與用戶線程
Java 中線程分為兩類,分別為 Daemon 線程(守護線程)和 User 線程(用戶線程),在 JVM 啟動時候會調用 main 函數,main 函數所在的線程是一個用戶線程,這個是我們可以看到的線程,其實 JVM 內部同時還啟動了好多守護線程,比如垃圾回收線程(嚴格說屬于 JVM 線程)。
那么守護線程和用戶線程有什么區別那?區別之一是當最后一個非守護線程結束時候,JVM 會正常退出,而不管當前是否有守護線程;也就是說守護線程是否結束并不影響 JVM 的退出。言外之意是只要有一個用戶線程還沒結束正常情況下 JVM 就不會退出。
那么 Java 中如何創建一個守護線程呢?代碼如下:
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
public void run() {
}
});
//設置為守護線程
daemonThread.setDaemon(true);
daemonThread.start();
}
可知只需要設置線程的 daemon 參數為 true 即可。
下面通過例子來加深用戶線程與守護線程的區別的理解,首先看下面代碼:
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
for(;;){}
}
});
//啟動子線
thread.start();
System.out.print("main thread is over");
}
結果輸出為:
如上代碼在 main 線程中創建了一個 thread 線程,thread 線程里面是無限循環,運行代碼從結果看 main 線程已經運行結束了,那么 JVM 進程已經退出了?從 IDE 的輸出結果右側上的紅色方塊說明 JVM 進程并沒有退出,另外 Mac 上執行 ps -eaf | grep java
會輸出結果,也可以證明這個結論。
這個結果說明了當父線程結束后,子線程還是可以繼續存在的,也就是子線程的生命周期并不受父線程的影響。也說明了當用戶線程還存在的情況下 JVM 進程并不會終止。
那么我們把上面的 thread 線程設置為守護線程后在運行看看會有什么效果:
//設置為守護線程
thread.setDaemon(true);
//啟動子線
thread.start();
執行結果為:
如上在啟動線程前設置線程為守護線程,從輸出結果可知 JVM 進程已經終止了,執行 ps -eaf |grep java
也看不到 JVM 進程了。這個例子里面 main 函數是唯一的用戶線程,thread 線程是守護線程,當 main 線程運行結束后,JVM 發現當前已經沒有用戶線程了,就會終止 JVM 進程。
Java 中在 main 線程運行結束后,JVM 會自動啟動一個叫做 DestroyJavaVM 線程,該線程會等待所有用戶線程結束后終止 JVM 進程,下面通過簡單的 JVM 代碼來證明這個結論:
翻開 JVM 的代碼,最終會調用到 JavaMain 這個函數:
int JNICALL
JavaMain(void * _args)
{
...
//執行Java中的main函數
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
//main函數返回值
ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;
//等待所有非守護線程結束,然后銷毀JVM進程
LEAVE();
}
LEAVE 是 C 語言里面的一個宏定義,定義如下:
#define LEAVE() \
do { \
if ((*vm)->DetachCurrentThread(vm) != JNI_OK) { \
JLI_ReportErrorMessage(JVM_ERROR2); \
ret = 1; \
} \
if (JNI_TRUE) { \
(*vm)->DestroyJavaVM(vm); \
return ret; \
} \
} while (JNI_FALSE)
上面宏的作用實際是創建了一個名字叫做 DestroyJavaVM 的線程來等待所有用戶線程結束。
在 Tomcat 的 NIO 實現 NioEndpoint 中會開啟一組接受線程用來接受用戶的鏈接請求和一組處理線程負責具體處理用戶請求,那么這些線程是用戶線程還是守護線程呢?下面我們看下 NioEndpoint 的 startInternal 方法:
public void startInternal() throws Exception {
if (!running) {
running = true;
paused = false;
...
//創建處理線程
pollers = new Poller[getPollerThreadCount()];
for (int i=0; i<pollers.length; i++) {
pollers[i] = new Poller();
Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);//聲明為守護線程
pollerThread.start();
}
//啟動接受線程
startAcceptorThreads();
}
protected final void startAcceptorThreads() {
int count = getAcceptorThreadCount();
acceptors = new Acceptor[count];
for (int i = 0; i < count; i++) {
acceptors[i] = createAcceptor();
String threadName = getName() + "-Acceptor-" + i;
acceptors[i].setThreadName(threadName);
Thread t = new Thread(acceptors[i], threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());//設置是否為守護線程,默認為守護線程
t.start();
}
}
private boolean daemon = true;
public void setDaemon(boolean b) { daemon = b; }
public boolean getDaemon() { return daemon; }
如上代碼也就是說默認情況下接受線程和處理線程都是守護線程,這意味著當 Tomact 收到 shutdown 命令后 Tomact 進程會馬上消亡,而不會等處理線程處理完當前的請求。
注:如果你想在主線程結束后 JVM 進程馬上結束,那么創建線程的時候可以設置線程為守護線程,否則如果希望主線程結束后子線程繼續工作,等子線程結束后在讓 JVM 進程結束那么就設置子線程為用戶線程。
總結
本文作為 Java 并發編程之美系列的開篇,講解了多線程并發編程線程有關基礎知識,有了這些基礎,為后面研究并發編程的高級知識打下基礎。
本文首發于GitChat,未經授權不得轉載,轉載需與GitChat聯系。