Java?多線程并發(fā)LockSupport
概覽
這部分內容來自于這個類的注釋,簡單翻譯了下。
LockSupport
類是用于創(chuàng)建鎖和其他同步類的基本線程阻塞原語。
它的實現(xiàn)思想是給每個使用它的線程頒發(fā)一個許可,當許可是可用狀態(tài)時(線程有許可),調用 park 方法會消耗一個許可,方法立即返回(與信號量的作用類似),線程可以繼續(xù)執(zhí)行 park 方法后面的邏輯;如果調用 park 方法前,許可處于不可用狀態(tài)(線程沒有許可),park 方法不會立即返回,從而導致線程阻塞。而此時,可以通過調用 unpark 方法使許可恢復到可用狀態(tài)(但與信號量不同,許可不會累積。最多有一個)。
方法 park 和 unpark 提供了阻塞和解除阻塞線程的有效方法,這些線程不會遇到 Thread.suspend
和 Thread.resume
存在的問題(因suspend 容易導致死鎖,這倆個方法因為這個原因已經(jīng)被棄用 ),因為 park 和 unpark 調用的線程,不存在鎖競爭。
而如果調用 park 方法的線程被中斷,park 方法將會立即 return ,并且有設置超時版本的 park 方法。park 方法也可以在任何其他時間沒有原因的 return,因此通常必須在返回時重新檢查條件的循環(huán)中調用。從這個意義上說,park 是對“繁忙等待”的優(yōu)化,它不會浪費太多時間旋轉,但必須與 unpark 配對才能有效。
park 方法有對應帶有 blocker 對象參數(shù)的重載方法, blocker 對象在線程被阻塞時被記錄,以允許監(jiān)視和診斷工具識別線程被阻塞的原因。 (此類工具可以使用 getBlocker(Thread) 方法訪問阻止程序。)強烈建議使用這些表單而不是沒有此參數(shù)的原始表單。 在鎖實現(xiàn)中作為阻塞器提供的正常參數(shù)是 this。
本質上 LockSupport 實現(xiàn)了一種自旋,構造類似于:
while (!canProceed()) { ... LockSupport.park(this); }
其中 `canProceed 或在調用之前的任何其他操作都不會導致鎖定或阻塞。 因為每個線程只有一個許可,所以任何對 park 的中間使用都可能會干擾其預期效果。
示例用法。 這是一個先進先出不可重入鎖類的草圖:
class FIFOMutex { ? private final AtomicBoolean locked = new AtomicBoolean(false); ? private final Queue<Thread> waiters ? ? = new ConcurrentLinkedQueue<Thread>(); ? public void lock() { ? ? boolean wasInterrupted = false; ? ? Thread current = Thread.currentThread(); ? ? waiters.add(current); ? ? // Block while not first in queue or cannot acquire lock ? ? while (waiters.peek() != current || ? ? ? ? ? ?!locked.compareAndSet(false, true)) { ? ? ? LockSupport.park(this); ? ? ? if (Thread.interrupted()) // ignore interrupts while waiting ? ? ? ? wasInterrupted = true; ? ? } ? ? waiters.remove(); ? ? if (wasInterrupted) ? ? ? ? ?// reassert interrupt status on exit ? ? ? current.interrupt(); ? } ? public void unlock() { ? ? locked.set(false); ? ? LockSupport.unpark(waiters.peek()); ? } }
源碼分析
整個 LockSupport 類的代碼量還算少,去掉注釋僅有 100 行,所有的屬性和方法都是靜態(tài)的,并且備注明確說明了 LockSupport 無法實例化:
public class LockSupport { ? ?private LockSupport() {} // 無法實例化 ? ?// ... }
LockSupport 中包含了幾個私有的內部靜態(tài)屬性:
public class LockSupport { // 通過內部 API 實現(xiàn) Hotspot ? ?private static final Unsafe U = Unsafe.getUnsafe(); ? ?private static final long PARKBLOCKER= U.objectFieldOffset(Thread.class, "parkBlocker"); ? ?private static final long TID = U.objectFieldOffset(Thread.class, "tid"); // ... }
從這些內部私有的靜態(tài)屬性可以看出,最重要的就是 U
了,LockSupport 中的方法,本質上也是調用U
提供的能力。U
在 CAS 與原子類中有介紹,是 JDK 中提供的一些非阻塞線程安全的實現(xiàn)能力的類,它的大多數(shù)方法都是 native 方法。
靜態(tài)方法
LockSupport 基本上就是個靜態(tài)工具類,它的主要能力,集中在它的靜態(tài)方法中。
public class LockSupport { public static void unpark(Thread thread) public static void park(Object blocker) public static void parkNanos(Object blocker, long nanos) public static void parkUntil(Object blocker, long deadline) public static Object getBlocker(Thread t) ? ?public static void setCurrentBlocker(Object blocker) ? ?private static void setBlocker(Thread t, Object arg) public static void park() public static void parkNanos(long nanos) public static void parkUntil(long deadline) ? ?static final long getThreadId(Thread thread) }
從方法名就可以看出,主要分為三個:
- unpark
- park
- getBlocker
Blocker
public static Object getBlocker(Thread t) { ? ?if (t == null) ? ? ? ?throw new NullPointerException(); ? ?return U.getReferenceOpaque(t, PARKBLOCKER); } private static void setBlocker(Thread t, Object arg) { ? ?U.putReferenceOpaque(t, PARKBLOCKER, arg); } // 在 JDK 14 之前是不存在該方法的, setBlocker 只能從內部進行 public static void setCurrentBlocker(Object blocker) { ? ?U.putReferenceOpaque(Thread.currentThread(), PARKBLOCKER, blocker); }
setCurrentBlocker
的作用是,設置當前線程調用 getBlocker
返回的對象。 在 JDK 14 后暴露這個方法的用途,是用來配合park()
的無參數(shù)版本設置 Blocker ,它可以實現(xiàn) park(blocker)
的效果:
setCurrentBlocker(b); park(); setCurrentBlocker(null);
而私有靜態(tài)方法 setBlocker
是在 park 的有參數(shù)方法中封裝使用的。
對于 blocker
的保存,本質是通過 Unsafe
的 putReferenceOpaque
方法保存和 getReferenceOpaque
方法讀取的。
@IntrinsicCandidate public final void putReferenceOpaque(Object o, long offset, Object x) { ? ?putReferenceVolatile(o, offset, x); } // 使用 volatile 存儲語義將引用值存儲到給定的 Java 變量中。 否則等同于 putReference(Object, long, Object) @IntrinsicCandidate public native void putReferenceVolatile(Object o, long offset, Object x); @IntrinsicCandidate public final Object getReferenceOpaque(Object o, long offset) { ? ?return getReferenceVolatile(o, offset); } // 從給定的 Java 變量中獲取引用值,具有可變加載語義。 否則等同于 getReference(Object, long) @IntrinsicCandidate public native Object getReferenceVolatile(Object o, long offset);
在 src/hotspot/share/opto/library_call.cpp
中發(fā)現(xiàn)了 Java 到 native 方法的映射:
case vmIntrinsics::_putReferenceVolatile: return inline_unsafe_access( is_store, T_OBJECT, ? Volatile, false);
native 最終調用到是老朋友 inline_unsafe_access
。(這里不詳細展開了。。我也沒搞明白這個方法,和匯編指令相關)
需要注意的是,最終的都是將對象設置為了 volatile
。充分說明 LockSupport
也是一套非阻塞同步方案。
而上面提到的 putReference(Object, long, Object)
的作用是:將引用值存儲到給定的 Java 變量中。
@IntrinsicCandidate public native void putReference(Object o, long offset, Object x);
除非存儲的引用 x
為 null 或與字段類型匹配,否則結果是未定義的。 如果引用 o
不為空,則更新該對象的卡片標記或其他存儲屏障(如果 VM 需要它們)。
unpark
public static void unpark(Thread thread) { if (thread != null) U.unpark(thread); }
解除調用 park 的線程的阻塞狀態(tài),或者如果調用 park 的線程沒有阻塞,則會導致后續(xù)調用 park 不會造成阻塞。
注意:這個操作是不安全的,調用者必須確保線程沒有被銷毀。
這是什么意思呢?通過下面這個例子可以感受到先調用 unpark 后,指定參數(shù)中的線程參數(shù)對象調用 park 不會造成阻塞:
class LockSupportDemo { ? ?fun check() { ? ? ? ?val thread1 = Thread { ? ? ? ? ? ?Thread.sleep(1000) ? ? ? ? ? ?println("thread1 start + ${Date(System.currentTimeMillis())}") ? ? ? ? ? ?LockSupport.park() ? ? ? ? ? ?println("thread1 end + ${Date(System.currentTimeMillis())}") ? ? ? } ? ? ? ? ?val thread2 = Thread { ? ? ? ? ? ?println("thread2 start + ${Date(System.currentTimeMillis())}") ? ? ? ? ? ?LockSupport.unpark(thread1) ? ? ? ? ? ?println("thread2 end + ${Date(System.currentTimeMillis())}") ? ? ? } ? ? ? ?thread1.start() ? ? ? ?thread2.start() ? } }
打印日志:
thread2 start + Fri Jun 03 02:19:34 CST 2022
thread2 end + Fri Jun 03 02:19:34 CST 2022
thread1 start + Fri Jun 03 02:19:35 CST 2022
thread1 end + Fri Jun 03 02:19:35 CST 2022
thread2 優(yōu)先執(zhí)行,thread1 在 thread2 開始執(zhí)行 1s 后執(zhí)行,thread1 調用了 LockSupport.park()
,并沒有造成自身阻塞。
Unsafe 的 unpark 方法
LockSupport.unpark(thread)
內部實際只調用了 Unsafe#unpark(thread);
:
public native void unpark(Object thread); 復制代碼
又是一個 native 方法。在 JDK 中發(fā)現(xiàn) Parker::unpark
的定義在 os_posix.cpp
和 on_windows.cpp
中:
可以看出這個 native 方法,應該在不同的平臺會有不同的實現(xiàn)。
park
park 方法有兩組重載方法:
// 不帶 blocker public static void park() public static void parkNanos(long nanos) public static void parkUntil(long deadline) // 需要 blocker 參數(shù) public static void park(Object blocker) public static void parkNanos(Object blocker, long nanos) public static void parkUntil(Object blocker, long deadline)
不帶 blocker 參數(shù)的分組
沒有 blocker
參數(shù)的一組本質上的邏輯是:
U.park(boolean, long); 復制代碼
這一點可以從三個方法中看出:
public static void park() { U.park(false, 0L); } public static void parkNanos(long nanos) { ? ?if (nanos > 0) ? ? ? ?U.park(false, nanos); } public static void parkUntil(long deadline) { ? ?U.park(true, deadline); }
這三個方法的區(qū)別是:
park
方法的作用是:除非許可處于可用狀態(tài),否者關閉當前線程的線程調度的意圖。如果許可可用,則許可被使用掉,并立即返回。否則當前線程因為線程調度的意圖被關閉而導致阻塞。
直到發(fā)生以下三種情況之一:
其他線程以當前線程為目標調用 unpark
其他線程中斷當前線程
調用虛假地 return(即 no reason)
這個方法不會報告是哪一種原因導致的 return。調用者應該重新檢查導致線程第一次停止的條件。 例如,調用者還可以確定線程在返回時的中斷狀態(tài)。
parkNanos
禁用當前線程的線程調度意圖,直到指定的等待時間,除非許可可用。如果參數(shù) nanos
為 0 或為負數(shù),這個方法將不會做任何事情。
parkUntil
禁用當前線程的線程調度意圖,直到指定的最后期限,除非許可可用。參數(shù) deadline
是截止日期——從 Epoch 開始等待的絕對時間,以毫秒為單位。
需要 blocker 參數(shù)的分組
public static void park(Object blocker) { ? ?Thread t = Thread.currentThread(); ? ?setBlocker(t, blocker); ? ?U.park(false, 0L); ? ?setBlocker(t, null); } public static void parkNanos(Object blocker, long nanos) { ? ?if (nanos > 0) { ? ? ? ?Thread t = Thread.currentThread(); ? ? ? ?setBlocker(t, blocker); ? ? ? ?U.park(false, nanos); ? ? ? ?setBlocker(t, null); ? } } public static void parkUntil(Object blocker, long deadline) { ? ?Thread t = Thread.currentThread(); ? ?setBlocker(t, blocker); ? ?U.park(true, deadline); ? ?setBlocker(t, null); }
帶有 blocker
參數(shù)的這組函數(shù),它們和對應的不帶參數(shù)的方法意義是一樣的,多了一個設置對象的操作。而這個對象 blocker
是負責此線程 parking 的同步對象 。
這組重載方法有共同的邏輯:
- 獲取當前線程對象。
- 將
blocker
對象的引用值存儲到線程對象中。 - 調用 Unsafe 對象的
park(boolean, long)
方法。 - 將當前線程對象存儲的引用值設置為 null 。
讓我困惑的是,這個存儲引用值操作有什么作用。但是聯(lián)想到線程操作和對象,驚奇的發(fā)現(xiàn)我們常用的 API Object.wait()
和 Object.notify()
,好像和這個場景很像,都用到了一個 Object
,也都造成了線程阻塞喚醒。
park/unpark 和 Object 的 wait/notify
public class LockSupportJava { ? ?Object obj = new Object();? ? ?public static void main(String[] args) { ? ? ? ?LockSupportJava lock = new LockSupportJava(); ? ? ? ?lock.waitAndNotify(); ? } ? ?void waitAndNotify() { ? ? ? ?Thread thread1 = new Thread(() -> { ? ? ? ? ? ?synchronized(obj) { ? ? ? ? ? ? ? ?try { ? ? ? ? ? ? ? ? System.out.println("thread1 start + " + new Date(System.currentTimeMillis())); ? ? ? ? ? ? ? ? ? ?Thread.sleep(1000); ? ? ? ? ? ? ? ? ? ?obj.notify(); ? ? ? ? ? ? ? System.out.println("thread1 end + " + new Date(System.currentTimeMillis())); ? ? ? ? ? ? ? } catch (InterruptedException e) { ? ? ? ? ? ? ? ? ? ?e.printStackTrace(); ? ? ? ? ? ? ? } ? ? ? ? ? } ? ? ? });? ? ? ? ?Thread thread2 = new Thread(() -> { ? ? ? ? ? ?synchronized(obj) { ? ? ? ? ? ? ? ?try { ? ? ? ? ? ? ? ? ? ?thread1.start(); ? ? ? ? ? ? ? ? ? ?Thread.sleep(3000); ? ? ? ? ? ? ? ? ? ?System.out.println("thread2 start + " + new Date(System.currentTimeMillis())); ? ? ? ? ? ? ? ? ? ?obj.wait(); ? ? ? ? ? ? ? ? ? ?System.out.println("thread2 end + " + new Date(System.currentTimeMillis())); ? ? ? ? ? ? ? } catch (InterruptedException e) { ? ? ? ? ? ? ? ? ? ?e.printStackTrace(); ? ? ? ? ? ? ? } ? ? ? ? ? } ? ? ? }); ? ? ? ?thread2.start(); ? } }
這是一個簡單的 Demo ,創(chuàng)建了兩個線程 thread1
和 thread2
。
線程 2 從主線程先執(zhí)行,持有了 obj
對象的鎖,在它的執(zhí)行邏輯中:
- 先啟動線程 1
- 線程 2 睡眠 3 秒
- 三秒后打印 start,obj 對象調用 wait ,使當前線程讓出對象的鎖,并進入阻塞狀態(tài)。
此時,線程 1 可以獲取到 obj 對象的鎖了,它的執(zhí)行邏輯:
- 線程 1 開始打印 start
- 睡眠 1 秒
- obj 對象調用 notify ,線程 2 開始嘗試獲取 obj 的鎖。
- 打印 end ,執(zhí)行結束,讓出 obj 的鎖
最后,線程 2 重新獲取到了 obj 的鎖,繼續(xù)執(zhí)行打印 end 。
從打印日志中,驗證打印順序:
thread2 start + Fri Jun 03 04:02:17 CST 2022
thread1 start + Fri Jun 03 04:02:17 CST 2022
thread1 end + Fri Jun 03 04:02:18 CST 2022
thread2 end + Fri Jun 03 04:02:18 CST 2022
注意:使用wait/notify實現(xiàn)同步時,必須先調用wait,后調用notify,如果先調用notify,再調用wait,會導致線程一直阻塞。
而如果使用 park / unpark 實現(xiàn)一個阻塞喚醒效果:
? ?Thread thread; ? ?void parkAndUnpark() { ? ? ? ?Thread thread2 = new Thread(() -> { ? ? ? ? ? ?System.out.println("thread2 start + " + new Date(System.currentTimeMillis())); ? ? ? ? ? ?thread.start(); ? ? ? ? ? ?LockSupport.park(obj); ? ? ? ? ? ?System.out.println("Blocker info " + LockSupport.getBlocker(Thread.currentThread())); ? ? ? ? ? ?System.out.println("thread2 end + " + new Date(System.currentTimeMillis())); ? ? ? }); ? ? ? ?// 喚起 thread2 ? ? ? ?Thread thread1 = new Thread(() -> { ? ? ? ? ? ?try { ? ? ? ? ? ? ? ?System.out.println("thread1 start + " + new Date(System.currentTimeMillis())); ? ? ? ? ? ? ? ?Thread.sleep(3000); ? ? ? ? ? ? ? ?System.out.println("Blocker info " + LockSupport.getBlocker(thread2)); ? ? ? ? ? ? ? ?LockSupport.unpark(thread2); ? ? ? ? ? ? ? ?System.out.println("thread1 end + " + new Date(System.currentTimeMillis())); ? ? ? ? ? } catch (InterruptedException e) { ? ? ? ? ? ? ? ?e.printStackTrace(); ? ? ? ? ? } ? ? ? }); ? ? ? ?thread = thread1; ? ? ? ?thread2.start(); ? }
parkAndUnpark()
方法中,先啟動了線程 2,線程 2 打印完 start 后就啟動了線程 1 。而此時線程 2 繼續(xù)執(zhí)行:
- 調用 park 進入阻塞狀態(tài)
在線程 2 執(zhí)行上面兩個邏輯的同時,線程 1 也在同時執(zhí)行:
- 線程 1 啟動后先打印 start
- 線程 1 睡眠 3 秒
- 檢查線程 2 調用
park(Object)
方法設置的 Blocker 信息 - 調用
LockSupport.unpark(thread2)
解除線程 2 的阻塞 - 線程 1 打印 end 執(zhí)行結束
線程 2 在被線程 1 喚醒后,繼續(xù)執(zhí)行打印信息:
- 打印當前線程的 Blocker 信息,為 null
- 打印 end 執(zhí)行結束
打印日志:
thread2 start + Fri Jun 03 04:24:32 CST 2022
thread1 start + Fri Jun 03 04:24:32 CST 2022
Blocker info java.lang.Object@7a3acdd9
thread1 end + Fri Jun 03 04:24:35 CST 2022
Blocker info null
thread2 end + Fri Jun 03 04:24:35 CST 2022
注意:先調用 unpark 后調用 park 也不會導致阻塞,更加靈活。
可以發(fā)現(xiàn),blocker
在線程恢復后變成了 null ,個人理解它的作用就是用來做阻塞標記的,可以用來在線程阻塞狀態(tài)下,將一個對象設置上,然后在其他線程中讀取這個對象。blocker
與線程的單次阻塞狀態(tài)綁定,可以用于對線程狀態(tài)的排查和線程狀態(tài)的監(jiān)控。
區(qū)別
從 wait/notify 和 park/unpark 兩種阻塞線程和喚起線程的方式,能夠感受出兩者的不同之處:
- wait/notify 需要配合 synchronized 進行;park/unpark 雖然設置了 blocker 但全程沒有鎖。
- wait/notify 需要保證 wait 在前,notify 在后,否則會阻塞線程;park/unpark 對調用順序沒有限制。不會造成阻塞。
- wait/notify 方法在 Object 中定義,用于對象鎖的資源讓出;park/unpark 來自于
LockSupport
,是靜態(tài)方法,用于對線程本身進行掛起喚醒。并可以通過blocker
綁定線程阻塞狀態(tài)下的一些信息。
個人感覺 park/unpark 更像是 掛起/恢復,而 Thread.suspend
和 Thread.resume
都已廢棄,是很好的替代方案。
suspend
,掛起線程,但是不會釋放類似鎖這樣的資源。resume
,恢復線程,如果之前沒有使用suspend暫停線程,則不起作用。Thread.stop()
由于其固有的風險而被逐步淘汰。當你停止一個線程時,它會解鎖它鎖定的所有監(jiān)視器。如果以前受這些監(jiān)視器保護的任何對象處于不一致狀態(tài),其他線程可能會看到這些對象處于不一致狀態(tài)。 作用在受損物體上的線可能會有意或無意地行為不規(guī)律。與其他不受控制的異常不同,ThreadDeath 會靜默地殺死線程,不會向用戶發(fā)出程序可能已損壞的警告。損壞發(fā)生后,損壞可能會在無法預料的時刻出現(xiàn)。此外,在多線程環(huán)境中使用 DBMS – JDBC 時,終止線程會產(chǎn)生問題。Thread.suspend()
已被棄用,因為它本質上容易死鎖。因此,Thread.resume()
也必須被棄用。當目標線程被掛起時,它會在監(jiān)視器上鎖定一個保護關鍵系統(tǒng)資源的鎖,并且在目標線程恢復之前沒有其他線程可以訪問它。如果將重新啟動目標線程的線程在調用 resume() 之前嘗試鎖定此監(jiān)視器,則會發(fā)生死鎖。
到此這篇關于Java 多線程并發(fā)LockSupport的文章就介紹到這了,更多相關Java LockSupport內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
解決swaggerUI頁面沒有顯示Controller方法的坑
這篇文章主要介紹了解決swaggerUI頁面沒有顯示Controller方法的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06Spring中DAO被循環(huán)調用的時候數(shù)據(jù)不實時更新的解決方法
這篇文章主要介紹了Spring中DAO被循環(huán)調用的時候數(shù)據(jù)不實時更新的解決方法,需要的朋友可以參考下2014-08-08SpringCloud Hystrix-Dashboard儀表盤的實現(xiàn)
這篇文章主要介紹了SpringCloud Hystrix-Dashboard儀表盤的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-08-08