Java——聊聊JUC中的线程中断机制 LockSupport
文章目錄:
1.什么是中斷機制?
2.如何停止中斷運行中的線程?
2.1?通過一個volatile變量實現
2.2?通過AtomicBoolean原子布爾類
2.3?通過Thread類自帶的中斷API方法實現
3.Thread類的三大API說明
3.1?實例方法interrupt(),沒有返回值
3.2?實例方法isInterrupted(),返回布爾值
3.3 當前線程的中斷標識為true,是不是線程就立刻停止?
3.4 在3.3中斷程序的基礎上,添加sleep睡眠
3.5 靜態方法public static?boolean?interrupted()
4.LockSupport
4.1 線程的等待喚醒機制
4.2 wait、notify
4.3 await、signal
4.4 park、unpark
1.什么是中斷機制?
- 首先,一個線程不應該由其他線程來強制中斷或停止,而是應該由線程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已經被廢棄了。
- 其次,在Java中沒有辦法立即停止一條線程,然而停止線程卻顯得尤為重要,如取消一個耗時操作。
? ? ? ? ? ? ? ? ? 因此,Java提供了一種用于停止線程的協商機制——中斷。
? ? ? ? ? ? ? ? ? 中斷只是一種協作協商機制,Java沒有給中斷增加任何語法,中斷的過程完全需要程序員自己實現。
? ? ? ? ? ? ? ? ? 若要中斷一個線程,你需要手動調用該線程的interrupt方法,該方法也僅僅是將線程對象的中斷標識設成true;
? ? ? ? ? ? ? ? ? 接著你需要自己寫代碼不斷地檢測當前線程的標識位,如果為true,表示別的線程要求這條線程中斷,此時究竟該做什么需要你自己寫代碼實現。
? ? ? ? ? ? ? ? ? 每個線程對象中都有一個標識,用于表示線程是否被中斷;該標識位為true表示中斷,為false表示未中斷;
? ? ? ? ? ? ? ? ? 通過調用線程對象的interrupt方法將該線程的標識位設為true;可以在別的線程中調用,也可以在自己的線程中調用。
借尚硅谷周陽老師的例子:顧客在無煙餐廳中吸煙,服務員希望他別吸煙了,不是強行停止他吸煙,而是給他的標志位打為true,具體的停止吸煙還是要顧客自己停止。(體現了協商機制)
- 中斷相關的三大API方法如下圖:↓↓↓
2.如何停止中斷運行中的線程?
2.1?通過一個volatile變量實現
package com.szh.demo.interrupt;import java.util.concurrent.TimeUnit;public class InterruptDemo1 {static volatile boolean isStop = false;public static void main(String[] args) {new Thread(() -> {while (true) {if (isStop) {System.out.println(Thread.currentThread().getName() + " isStop被修改為true,線程停止");break;}System.out.println(Thread.currentThread().getName() + " hello volatile....");}}, "t1").start();try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {isStop = true;}, "t2").start();} }2.2?通過AtomicBoolean原子布爾類
package com.szh.demo.interrupt;import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean;public class InterruptDemo2 {static AtomicBoolean atomicBoolean = new AtomicBoolean(false);public static void main(String[] args) {new Thread(() -> {while (true) {if (atomicBoolean.get()) {System.out.println(Thread.currentThread().getName() + " isStop被修改為true,線程停止");break;}System.out.println(Thread.currentThread().getName() + " hello AtomicBoolean....");}}, "t1").start();try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {atomicBoolean.set(true);}, "t2").start();} }2.3?通過Thread類自帶的中斷API方法實現
package com.szh.demo.interrupt;import java.util.concurrent.TimeUnit;public class InterruptDemo3 {public static void main(String[] args) {Thread t1 = new Thread(() -> {while (true) {if (Thread.currentThread().isInterrupted()) {System.out.println(Thread.currentThread().getName() + " isStop被修改為true,線程停止");break;}System.out.println(Thread.currentThread().getName() + " hello isInterrupted....");}}, "t1");t1.start();try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {t1.interrupt();}, "t2").start();} }3.Thread類的三大API說明
3.1?實例方法interrupt(),沒有返回值
這個interrupt()實例方法,底層實際上調用了interrupt0()這個方法,根據后面的注釋可以看到,僅僅是設置中斷標識位,而interrupt0這個方法是一個native方法,底層又調用了C。
而在jdk官方文檔中可以看到有關這個方法的敘述。
3.2?實例方法isInterrupted(),返回布爾值
這個實例方法的底層調用了一個native方法,傳入了一個布爾值,而這個值就是 是否清除中斷標識位,false表示不清除,true表示清除(即將線程的中斷標識位清除重新設置為false)。
具體來說,當對一個線程,調用 interrupt() 時:
① 如果線程處于正常活動狀態,那么會將該線程的中斷標志設置為 true,僅此而已。被設置中斷標志的線程將繼續正常運行,不受影響。所以, interrupt() 并不能真正的中斷線程,需要被調用的線程自己進行配合才行。
② 如果線程處于被阻塞狀態(例如處于sleep, wait, join 等狀態),在別的線程中調用當前線程對象的interrupt方法,那么線程將立即退出被阻塞狀態(中斷狀態將被清除),并拋出一個InterruptedException異常。
(中斷不活動的線程不會產生任何影響,看下面案例)
3.3 當前線程的中斷標識為true,是不是線程就立刻停止?
-
否,僅僅設置了一個中斷狀態
-
看看中斷是否會立即停止這個300的線程。否,雖然中斷標志位變了。但是i一直輸完300次,才最終停止。
對上面的代碼稍作改變,如下:↓↓↓
package com.szh.demo.interrupt;import java.util.concurrent.TimeUnit;public class InterruptDemo5 {public static void main(String[] args) {Thread t1 = new Thread(() -> {for (int i = 0; i < 300; i++) {System.out.println("--------- " + i);}System.out.println("after t1.interrupt()---第2次---- " + Thread.currentThread().isInterrupted());}, "t1");t1.start();System.out.println("before t1.interrupt()---- " + t1.isInterrupted());try {TimeUnit.MILLISECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}t1.interrupt();System.out.println("after t1.interrupt()---第1次--- " + t1.isInterrupted());try {TimeUnit.MILLISECONDS.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("after t1.interrupt()---第3次--- " + t1.isInterrupted());} }在輸出結果中,我們可以看到和我們預想的都一樣,只有最后一行輸出,t1線程它自己不是已經打斷了嗎?那中斷標識就應該是 true 啊?為什么變成了false???
原因是上面的代碼中,t1線程打印300次i,而最后一行輸出代碼是在2000ms之后的,t1線程是完全可以在這個時間內完成300次i的打印工作,所以程序運行到最后一行輸出,t1線程已經結束死亡了,再根據 interrupt 方法api中的這句話:
-
-
-
中斷不存在的線程不需要任何效果。
-
-
我們就懂了,中斷不存在的線程沒什么意義的,所以這里的中斷標識自然就恢復成了默認值 false。
3.4 在3.3中斷程序的基礎上,添加sleep睡眠
package com.szh.demo.interrupt;import java.util.concurrent.TimeUnit;public class InterruptDemo6 {public static void main(String[] args) {Thread t1 = new Thread(() -> {while (true) {if (Thread.currentThread().isInterrupted()) {System.out.println(Thread.currentThread().getName() + " 中斷標識位:" +Thread.currentThread().isInterrupted() + " 線程終止....");break;}try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("---- hello InterruptDemo6");}}, "t1");t1.start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {t1.interrupt();}, "t2").start();} }這個程序是停不下來的,我是不想耗費太多CPU資源,手動停止了。?
原因就是:
-
-
-
如果該線程阻塞的調用wait() , wait(long) ,或wait(long, int)的方法Object類,或者在join() , join(long) , join(long, int) , sleep(long) ,或sleep(long, int) ,這個類的方法,那么它的中斷狀態將被清除,并且將收到一個InterruptedException 。
-
所以這個時候線程t1的中斷標識位被清除,恢復成了false,那么久永遠也停不下來了。
-
-
如何修改上面的代碼,使得程序正常運行停止呢?? ?→?
3.5 靜態方法public static?boolean?interrupted()
靜態方法,Thread.interrupted();判斷線程是否被中斷,并清除當前中斷狀態這個方法做了兩件事:1 返回當前線程的中斷狀態? ? 2 將當前線程的中斷狀態設為false(這個方法有點不好理解,因為連續調用兩次的結果可能不一樣。)
package com.szh.demo.interrupt;public class InterruptDemo7 {public static void main(String[] args) {System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());System.out.println("-----1");Thread.currentThread().interrupt();//中斷標志位設置為trueSystem.out.println("-----2");System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());} }前兩次調用沒啥說的,因為main主線程并沒有中斷,第三次調用的時候,因為上面已經 interrupt 了,所以被中斷了,這里中斷標識位肯定就是 true。此時這個靜態方法在中斷之后第一次調用(返回當前線程的中斷狀態,被中斷了就是true;第二件事,將當前線程的中斷標識重置為false)。所以當最后一行再次調用它的時候,就是false了。?
看一下這個靜態方法的源碼:↓↓↓? ? 在那個isInterrupt實例方法中傳入的 一個布爾值,而這個值就是 是否清除中斷標識位,false表示不清除,true表示清除(即將線程的中斷標識位清除重新設置為false)。
這兩個方法在底層都調用了native方法isInterrupted。? 只不過傳入參數ClearInterrupted一個傳參傳了true,一個傳了false。
靜態方法interrupted() 中true表示清空當前中斷狀態。? 實例方法isInterrupted 則不會。
4.LockSupport
用于創建鎖和其他同步類的基本線程阻塞原語。
這個類與每個使用它的線程相關聯,一個許可證(在Semaphore類的意義上)。 如果許可證可用,則呼叫park將park返回,在此過程中消耗它; 否則可能會阻止。 致電unpark使許可證可用,如果尚不可用。 (與信號量不同,許可證不能累積,最多只有一個。)
核心就是park()和unpark()方法
-
park()方法是阻塞線程
-
unpark()方法是解除阻塞線程
4.1 線程的等待喚醒機制
使用Object中的wait()方法讓線程等待,使用Object中的notify()方法喚醒線程。(有局限性)
使用JUC包中Condition的await()方法讓線程等待,使用signal()方法喚醒線程。(有局限性)
LockSupport類可以阻塞當前線程以及喚醒指定被阻塞的線程。
4.2 wait、notify
package com.szh.demo.locksupport;import java.util.concurrent.TimeUnit;public class LockSupportDemo1 {public static void main(String[] args) {final Object obj = new Object();new Thread(() -> {synchronized (obj) {System.out.println(Thread.currentThread().getName() + " --- come in");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + " --- 被喚醒了");}, "t1").start();try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {synchronized (obj) {obj.notify();System.out.println(Thread.currentThread().getName() + " --- 發出通知");}}, "t2").start();} }異常情況1:將 synchronized 同步代碼塊對應的代碼注釋掉。?
package com.szh.demo.locksupport;import java.util.concurrent.TimeUnit;public class LockSupportDemo1 {public static void main(String[] args) {final Object obj = new Object();new Thread(() -> {//synchronized (obj) {System.out.println(Thread.currentThread().getName() + " --- come in");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}//}System.out.println(Thread.currentThread().getName() + " --- 被喚醒了");}, "t1").start();try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {//synchronized (obj) {obj.notify();System.out.println(Thread.currentThread().getName() + " --- 發出通知");//}}, "t2").start();} }異常情況2:將wait和notify順序調換。?
package com.szh.demo.locksupport;import java.util.concurrent.TimeUnit;public class LockSupportDemo1 {public static void main(String[] args) {final Object obj = new Object();new Thread(() -> {try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}synchronized (obj) {System.out.println(Thread.currentThread().getName() + " --- come in");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + " --- 被喚醒了");}, "t1").start();// try { // TimeUnit.SECONDS.sleep(3L); // } catch (InterruptedException e) { // e.printStackTrace(); // }new Thread(() -> {synchronized (obj) {obj.notify();System.out.println(Thread.currentThread().getName() + " --- 發出通知");}}, "t2").start();} }小總結
-
線程先要獲得并持有鎖,必須在鎖塊(synchronized或lock)中
-
必須要先等待后喚醒,線程才能夠被喚醒。要保證先wait,后notify才OK。
-
wait和notify方法必須要在同步塊或者方法里面,且成對出現使用。
4.3 await、signal
package com.szh.demo.locksupport;import javax.swing.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;public class LockSupportDemo2 {private static Lock lock = new ReentrantLock();private static Condition condition = lock.newCondition();public static void main(String[] args) {new Thread(() -> {lock.lock();try {System.out.println(Thread.currentThread().getName() + " --- come in");condition.await();System.out.println(Thread.currentThread().getName() + " --- 被喚醒了");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}, "t1").start();try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {lock.lock();try {condition.signal();System.out.println(Thread.currentThread().getName() + " --- 發出通知");} finally {lock.unlock();}}, "t2").start();} }異常情況1:將對應的加鎖解鎖的代碼注釋掉,報錯信息和第一個案例是一樣的。?
異常情況2:先進行 signal,再進行 await,報錯信息和第一個案例是一樣的。?
小總結
-
線程先要獲得并持有鎖,必須在鎖塊(synchronized或lock)中
-
必須要先等待后喚醒,線程才能夠被喚醒。一定要先await后signal,不能反了
-
Condition中的線程等待和喚醒方法,需要先獲取鎖
4.4 park、unpark
調用LockSupport.park()時,發現它調用了unsafe類,并且默認傳了一個0。
permit默認是零,所以一開始調用park()方法,當前線程就會阻塞,直到別的線程將當前線程的permit設置為1時,park方法會被喚醒,然后會將permit再次設置為零并返回。
調用LockSupport.unpark();時,也調用了unsafe類。
調用unpark(thread)方法后,就會將thread線程的許可permit設置成1(注意多次調用unpark方法,不會累加,permit值還是1)會自動喚醒thread線程,即之前阻塞中的LockSupport.park()方法會立即返回。
解決上面兩個案例的第一個問題:必須放在鎖塊中,LockSupport不需要這樣做。?
package com.szh.demo.locksupport;import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport;public class LockSupportDemo3 {public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " --- come in");LockSupport.park();System.out.println(Thread.currentThread().getName() + " --- 被喚醒了");}, "t1");t1.start();try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {LockSupport.unpark(t1);System.out.println(Thread.currentThread().getName() + " --- 發出通知");}, "t2").start();} }解決上面兩個案例的第一個問題:必須先等待,后喚醒,LockSupport不需要這樣做,先喚醒后等待照樣OK。?
package com.szh.demo.locksupport;import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport;public class LockSupportDemo3 {public static void main(String[] args) {Thread t1 = new Thread(() -> {try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " --- come in " + System.currentTimeMillis());LockSupport.park();System.out.println(Thread.currentThread().getName() + " --- 被喚醒了 " + System.currentTimeMillis());}, "t1");t1.start();new Thread(() -> {LockSupport.unpark(t1);System.out.println(Thread.currentThread().getName() + " --- 發出通知");}, "t2").start();} }這里會先執行t2線程的unpark方法,此時t1線程手中就有了一張許可證,當t1線程睡眠3秒之后,執行代碼,走到park方法不會再阻塞,直接拿出許可證,繼續向下執行,所以看代碼的花費時間就知道,這里的park是無效沒有阻塞的。?
jdk官方文檔中說了,與信號量不同,許可證不能累積,最多只有一個。
老子就不信這個邪,我非得給你來兩個許可證,看看下面的代碼。
package com.szh.demo.locksupport;import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport;public class LockSupportDemo3 {public static void main(String[] args) {Thread t1 = new Thread(() -> {try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " --- come in " + System.currentTimeMillis());LockSupport.park();LockSupport.park();System.out.println(Thread.currentThread().getName() + " --- 被喚醒了 " + System.currentTimeMillis());}, "t1");t1.start();new Thread(() -> {LockSupport.unpark(t1);LockSupport.unpark(t1);System.out.println(Thread.currentThread().getName() + " --- 發出通知");}, "t2").start();} }可以看到,代碼卡在這里了,這是因為你雖然發了兩個許可證,但是最多只能持有一個,那么當第二次park嘗試再去獲取許可證時,已經不可能了,因為t1線程手中的那個許可證已經被第一次park的時候消費掉了。
當調用park方法時如果有憑證,則會直接消耗掉這個憑證然后正常退出;如果無憑證,就必須阻塞等待憑證可用。
而unpark則相反, 它會增加一個憑證, 但憑證最多只能有1個, 累加無效。?
針對park和unpark方法的代碼實測結論:
總結
以上是生活随笔為你收集整理的Java——聊聊JUC中的线程中断机制 LockSupport的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 三年级计算机之父童年教学设计,三年级语文
- 下一篇: 机器人学笔记之——操作臂运动学:驱动器空