執行緒中斷 interrupt 和 LockSupport
本文章將要介紹的內容有以下幾點,讀者朋友也可先自行思考一下相關問題:
- 執行緒中斷 interrupt 方法怎麼理解,意思就是執行緒中斷了嗎?那當前執行緒還能繼續執行嗎?
- 判斷執行緒是否中斷的方法有幾個,它們之間有什麼區別?
- LockSupport的 park/unpark 和 wait/notify 有什麼區別?
- sleep 方法是怎麼響應中斷的?
- park 方法又是怎麼響應中斷的?
執行緒中斷相關方法
執行緒中和中斷相關的方法有三個,分別介紹如下:
1) interrupt
我們一般都說這個方法是用來中斷執行緒的,那麼這個中斷應該怎麼理解呢? 就是說把當前正在執行的執行緒中斷掉,不讓它繼續往下執行嗎?
其實,不然。 此處,說的中斷僅僅是給執行緒設定一箇中斷的標識(設定為true),執行緒還是會繼續往下執行的。而執行緒怎麼停止,則需要由我們自己去處理。 一會兒會用程式碼來說明這個。
2) isInterrupted
判斷當前執行緒的中斷狀態,即判斷執行緒的中斷標識是true還是false。 注意,這個方法不會對執行緒原本的中斷狀態產生任何影響。
3) interrupted
也是判斷執行緒的中斷狀態的。但是,需要注意的是,這個方法和 isInterrupted 有很大的不同。我們看下它們的原始碼:
public boolean isInterrupted() { return isInterrupted(false); } public static boolean interrupted() { return currentThread().isInterrupted(true); } //呼叫同一個方法,只是傳參不同 private native boolean isInterrupted(boolean ClearInterrupted);
首先 isInterrupted 方法是執行緒物件的方法,而 interrupted 是Thread類的靜態方法。
其次,它們都呼叫了同一個本地方法 isInterrupted,不同的只是傳參的值,這個引數代表的是,是否要把執行緒的中斷狀態清除(清除即不論之前的中斷狀態是什麼值,最終都會設定為false)。
因此,interrupted 靜態方法會把原本執行緒的中斷狀態清除,而 isInterrupted 則不會。所以,如果你呼叫兩次 interrupted 方法,第二次就一定會返回false,除非中間又被中斷了一次。
下面證明一下 interrupt 方法只是設定一箇中斷狀態,而不是使當前執行緒中斷執行:
public class TestFlag {
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("執行緒中斷標誌:"+Thread.currentThread().isInterrupted());
while (flag){
}
System.out.println("標誌flag為:" + flag);
System.out.println("執行緒中斷標誌:"+Thread.currentThread().isInterrupted());
System.out.println("我還在繼續執行");
}
});
t.start();
Thread.sleep(100);
flag = false;
t.interrupt();
}
}
執行結果:
執行緒中斷標誌:false
標誌flag為:false
執行緒中斷標誌:true
我還在繼續執行
當執行緒啟動,還沒呼叫中斷方法時,中斷狀態為false,然後呼叫中斷方法,並把flag設定為false。此時,run方法跳出while死迴圈。我們會發現執行緒的中斷狀態為true,但是執行緒還是會繼續往下執行,直到執行結束。
sleep 響應中斷
執行緒中常用的阻塞方法,如sleep,join和wait 都會響應中斷,然後丟擲一箇中斷異常 InterruptedException。但是,注意此時,執行緒的中斷狀態會被清除。所以,當我們捕獲到中斷異常之後,應該保留中斷資訊,以便讓上層程式碼知道當前執行緒中斷了。通常有兩種方法可以做到。
一種是,捕獲異常之後,再重新丟擲異常,讓上層程式碼知道。另一種是,在捕獲異常時,通過 interrupt 方法把中斷狀態重新設定為true。
下面,就以sleep方法為例,捕獲中斷異常,然後重新設定中斷狀態:
public class TestInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
private int count = 0;
@Override
public void run() {
try {
count = new Random().nextInt(1000);
count = count * count;
System.out.println("count:"+count);
Thread.sleep(5000);
} catch (Exception e) {
System.out.println(Thread.currentThread().getName()+"執行緒第一次中斷標誌:"+Thread.currentThread().isInterrupted());
//重新把執行緒中斷狀態設定為true,以便上層程式碼判斷
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName()+"執行緒第二次中斷標誌:"+Thread.currentThread().isInterrupted());
}
}
});
t.start();
Thread.sleep(100);
t.interrupt();
}
}
結果:
count:208849
Thread-0執行緒第一次中斷標誌:false
Thread-0執行緒第二次中斷標誌:true
LockSupport方法介紹
LockSupport 方法中重要的兩個方法就是park 和 unpark 。
park和interrupt中斷
park方法可以阻塞當前執行緒,如果呼叫unpark方法或者中斷當前執行緒,則會從park方法中返回。
park方法對中斷方法的響應和 sleep 有一些不太一樣。它不會丟擲中斷異常,而是從park方法直接返回,不影響執行緒的繼續執行。我們看下程式碼:
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new ParkThread());
t.start();
Thread.sleep(100); //①
System.out.println(Thread.currentThread().getName()+"開始喚醒阻塞執行緒");
t.interrupt();
System.out.println(Thread.currentThread().getName()+"結束喚醒");
}
}
class ParkThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"開始阻塞");
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"第一次結束阻塞");
LockSupport.park();
System.out.println("第二次結束阻塞");
}
}
列印結果如下:
Thread-0開始阻塞
main開始喚醒阻塞執行緒
main結束喚醒
Thread-0第一次結束阻塞
第二次結束阻塞
當呼叫interrupt方法時,會把中斷狀態設定為true,然後park方法會去判斷中斷狀態,如果為true,就直接返回,然後往下繼續執行,並不會丟擲異常。注意,這裡並不會清除中斷標誌。
unpark
unpark會喚醒被park的指定執行緒。但是,這裡要說明的是,unpark 並不是簡單的直接去喚醒被park的執行緒。看下JDK的解釋:
unpark只是給當前執行緒設定一個許可證。如果當前執行緒已經被阻塞了(即呼叫了park),則會轉為不阻塞的狀態。如若不然,下次呼叫park方法的時候也會保證不阻塞。這句話的意思,其實是指,park和unpark的呼叫順序無所謂,只要unpark設定了這個許可證,park方法就可以在任意時刻消費許可證,從而不會阻塞方法。
還需要注意的是,許可證最多隻有一個,也就是說,就算unpark方法呼叫多次,也不會增加許可證。 我們可以通過程式碼驗證,只需要把上邊程式碼修改一行即可:
//LockSupportTest類
//原始碼
t.interrupt();
//修改為
LockSupport.unpark(t);
LockSupport.unpark(t);
就會發現,只有第一次阻塞會被喚醒,但是第二次依然會繼續阻塞。結果如下:
Thread-0開始阻塞
main開始喚醒阻塞執行緒
main結束喚醒
Thread-0第一次結束阻塞
另外,在此基礎上,把主執行緒的sleep方法去掉(程式碼中①處),讓主執行緒先執行,也就是有可能先呼叫unpark方法,然後子執行緒才開始呼叫park方法阻塞。我們會發現,出現以下結果,證明了上邊我說的park方法和unpark不分先後順序,park方法可以隨時消費許可證。
main開始喚醒阻塞執行緒
main結束喚醒
Thread-0開始阻塞
Thread-0第一次結束阻塞
park/unpark和 wait/notify區別
瞭解了 park/unpark的用法之後,想必你也能分析出來它們和 wait、notify有什麼不同之處了。
1) wait和notify方法必須和同步鎖 synchronized一塊兒使用。而park/unpark使用就比較靈活了,沒有這個限制,可以在任何地方使用。
2) park/unpark 使用時沒有先後順序,都可以使執行緒不阻塞(前面程式碼已驗證)。而wait必須在notify前先使用,如果先notify,再wait,則執行緒會一直等待。
3) notify只能隨機釋放一個執行緒,並不能指定某個特定執行緒,notifyAll是釋放鎖物件中的所有執行緒。而unpark方法可以喚醒指定的執行緒。
4) 呼叫wait方法會使當前執行緒釋放鎖資源,但使用的前提是必須已經獲得了鎖。 而park不會釋放鎖資源。(以下程式碼驗證)
public class LockSyncTest {
private static Object lock = new Object();
//儲存呼叫park的執行緒,以便後續喚醒
private static Thread parkedThread;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
synchronized (lock){
System.out.println("unpark前");
LockSupport.unpark(parkedThread);
System.out.println("unpark後");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//和t1執行緒用同一把鎖時,park不會釋放鎖資源,若換成this鎖,則會釋放鎖
synchronized (lock){
System.out.println("park前");
parkedThread = Thread.currentThread();
LockSupport.park();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("park後");
}
}
});
t2.start();
Thread.sleep(100);
t1.start();
}
}
//列印結果
//park前
以上程式碼,會一直卡在t2執行緒,因為park不會釋放鎖,因此t1也無法執行。
如果把t2的鎖換成this鎖,即只要和t1不是同一把鎖,則t1就會正常執行,然後把t2執行緒喚醒。列印結果如下:
park前
unpark前
unpark後
park後