JAVA高併發---LockSupport的學習及簡單使用
JAVA高併發—LockSupport的學習及簡單使用
1、簡單介紹
LockSupport是JDK中比較底層的類,用來建立鎖和其他同步工具類的基本執行緒阻塞原語。可以做到與join()
、wait()/notifyAll()
功能一樣,使執行緒自由的阻塞、釋放。
Java鎖和同步器框架的核心AQS(AbstractQueuedSynchronizer 抽象佇列同步器)
,就是通過呼叫LockSupport.park()
和LockSupport.unpark()
實現執行緒的阻塞和喚醒的。
補充:AQS定義了一套多執行緒訪問共享資源的同步器框架,許多同步類實現都依賴於它, 如常用的ReentrantLock/Semaphore/CountDownLatch...。
2、簡單原理
LockSupport
方法底層都是呼叫Unsafe
的方法實現。全名sun.misc.Unsafe,該類可以直接操控記憶體,被JDK廣泛用於自己的包中,如java.nio和java.util.concurrent。但是不建議在生產環境中使用這個類。因為這個API十分不安全、不輕便、而且不穩定。
LockSupport提供park()
和unpark()
方法實現阻塞執行緒和解除執行緒阻塞,LockSupport和每個使用它的執行緒都與一個許可(permit)關聯。permit是相當於1,0的開關,預設是0,呼叫一次unpark就加1變成1,呼叫一次park會消費permit, 也就會將1變成0,同時park立即返回。再次呼叫park會變成block(因為permit為0了,會阻塞在這裡,直到permit變為1), 這時呼叫unpark會把permit置為1。每個執行緒都有一個相關的permit, permit最多隻有一個,重複呼叫unpark也不會積累。意思就是說unpark
permit
已經變為1,之後,再執行unpark
,permit
依舊是1。下邊有例子會說到。
3、簡單例子
以下邊的做飯
例子,正常來說,做飯
之前,要有鍋
、有菜
才能開始做飯
。具體如下:
(1)先假設已經有了鍋
,那隻需要買菜
就可以做飯
。如下,即註釋掉了買鍋的步驟:
public class LockSupportTest { public static void main(String[] args) throws InterruptedException { //買鍋 // Thread t1 = new Thread(new BuyGuo(Thread.currentThread())); // t1.start(); //買菜 Thread t2 = new Thread(new BuyCai(Thread.currentThread())); t2.start(); // LockSupport.park(); // System.out.println("鍋買回來了..."); LockSupport.park(); System.out.println("菜買回來 了..."); System.out.println("開始做飯"); } } class BuyGuo implements Runnable{ private Object threadObj; public BuyGuo(Object threadObj) { this.threadObj = threadObj; } @Override public void run() { System.out.println("去買鍋..."); LockSupport.unpark((Thread)threadObj); } } class BuyCai implements Runnable{ private Object threadObj; public BuyCai(Object threadObj) { this.threadObj = threadObj; } @Override public void run() { System.out.println("買菜去..."); LockSupport.unpark((Thread)threadObj); } }
執行後,可出現下面的結果:
買菜去...
菜買回來了...
開始做飯
如上所述,可以達到阻塞主執行緒等到買完菜之後才開始做飯。這即是park()
、unpark()
的用法。簡單解釋一下上述的步驟:
main
方法啟動後,主執行緒
和買菜執行緒
同時開始執行。- 因為兩者同時進行,當
主執行緒
走到park()
時,發現permit
還為0
,即會等待在這裡。 - 當
買菜執行緒
執行進去後,走到unpark()
會將permit
變為1
。 主執行緒
park()
處發現permit
已經變成1
,就可以繼續往下執行了,同時消費掉permit
,重新變成0
。
以上permit
只是park/unpark
執行的一種邏輯開關,執行的步驟大致如此。
4、注意點及思考
(1)必須將park()
與uppark()
配對使用才更高效。
如果上邊也把買鍋
的執行緒放開,main
方法改為如下:
//買鍋
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
//買菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("鍋買回來了...");
LockSupport.park();
System.out.println("菜買回來了...");
System.out.println("開始做飯");
即呼叫了兩次park()
和unpark()
,發現有時候可以,有時候會使執行緒卡在那裡,然後我又換了下順序,如下:
//買鍋
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
LockSupport.park();
System.out.println("鍋買回來了...");
//買菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("菜買回來了...");
System.out.println("開始做飯");
原理沒有詳細去研究,不過想了想,上邊兩種其實並無區別,只是執行順序有了影響,park()
和unpark()
既然是成對配合使用,通過標識permit
來控制,如果像前邊那個例子那樣,出現阻塞的情況原因,我分析可能是這麼個原因:
當買鍋的時候,通過unpark()
將permit
置為1,但是還沒等到外邊的main方法執行第一個park()
,買菜的執行緒又調了一次unpark()
,但是這時候permit
還是從1變成了1,等回到主執行緒呼叫park()的時候,因為還有兩個park()
需要執行,也就是需要兩個消費permit
,因為permit
只有1個,所以,可能會剩下一個park()
卡在那裡了。
(2)使用park(Object blocker)
方法更能明確問題
其實park()
有個過載方法park(Object blocker)
,這倆方法效果差不多,但是有blocker的可以傳遞給開發人員更多的現場資訊,可以檢視到當前執行緒的阻塞物件,方便定位問題。所以java6新增加帶blocker入參的系列park方法,替代原有的park方法。
5、與wait()/notifyAll()
的比較
LockSupport
的 park/unpark
方法,雖然與平時Object
中wait/notify
同樣達到阻塞執行緒的效果。但是它們之間還是有區別的。
- 面向的物件主體不同。
LockSupport()
操作的是執行緒物件,直接傳入的就是Thread
,而wait()
屬於具體物件,notifyAll()
也是針對所有執行緒進行喚醒。 wait/notify
需要獲取物件的監視器,即synchronized
修飾,而park/unpark
不需要獲取物件的監視器。- 實現的機制不同,因此兩者沒有交集。也就是說
LockSupport
阻塞的執行緒,notify/notifyAll
沒法喚醒。但是park
之後,同樣可以被中斷(interrupt()) !