前言
Java語言中有許多原生線程安全的數據結構,比如ArrayBlockingQueue、CopyOnWriteArrayList、LinkedBlockingQueue,它們線程安全的實現方式並非通過synchronized關鍵字,而是通過java.util.concurrent.locks.ReentrantLock來實現。 剛好對這個很感興趣, 因此寫一篇博客詳細分析此 “可重入鎖實現原理”。
ReentrantLock的實現是基於其內部類FairSync(公平鎖)和NonFairSync(非公平鎖)實現的。 其可重入性是基於Thread.currentThread()實現的: 如果當前線程已經獲得了執行序列中的鎖, 那執行序列之后的所有方法都可以獲得這個鎖。
公平鎖:
公平和非公平鎖的隊列都基於鎖內部維護的一個雙向鏈表,表結點Node的值就是每一個請求當前鎖的線程。公平鎖則在於每次都是依次從隊首取值。
鎖的實現方式是基於如下幾點:
表結點Node和狀態state的volatile關鍵字。
sum.misc.Unsafe.compareAndSet的原子操作(見附錄)。
非公平鎖:
在等待鎖的過程中, 如果有任意新的線程妄圖獲取鎖,都是有很大的幾率直接獲取到鎖的。
ReentrantLock鎖都不會使得線程中斷,除非開發者自己設置了中斷位。
ReentrantLock獲取鎖里面有看似自旋的代碼,但是它不是自旋鎖。
ReentrantLock公平與非公平鎖都是屬於排它鎖。
ReentrantLock的可重入性分析
這里有一篇對鎖介紹甚為詳細的文章 朱小廝的博客-Java中的鎖.
synchronized的可重入性
參考這篇文章: Java內置鎖synchronized的可重入性
java線程是基於“每線程(per-thread)”,而不是基於“每調用(per-invocation)”的(java中線程獲得對象鎖的操作是以每線程為粒度的,per-invocation互斥體獲得對象鎖的操作是以每調用作為粒度的)
ReentrantLock的可重入性
前言里面提到,ReentrantLock重入性是基於Thread.currentThread()實現的: 如果當前線程已經獲得了鎖, 那該線程下的所有方法都可以獲得這個鎖。ReentrantLock的鎖依賴只有 NonfairSync和FairSync兩個實現類, 他們的鎖獲取方式大同小異。
可重入性的實現基於下面代碼片段的 else if 語句
1 protected final boolean tryAcquire(int acquires) { 2 final Thread current = Thread.currentThread(); 3 int c = getState(); 4 if (c == 0) { 5 ... // 嘗試獲取鎖成功
6 } 7 else if (current == getExclusiveOwnerThread()) { 8 // 是當前線程,直接獲取到鎖。實現可重入性。
9 int nextc = c + acquires; 10 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); 11 return true; 12 } 13 return false; 14 }
此處有兩個值需要關心:
1 /**
2 * The current owner of exclusive mode synchronization. 3 * 持有該鎖的當前線程 4 */ private transient Thread exclusiveOwnerThread; -----------------兩個值不在同一個類---------------- /**
5 * The synchronization state. 6 * 0: 初始狀態-無任何線程得到了鎖 7 * > 0: 被線程持有, 具體值表示被當前線程持有的執行次數 8 * 9 * 這個字段在解鎖的時候也需要用到。 10 * 注意這個字段的修飾詞: volatile 11 */ private volatile int state;
ReentrantLock鎖的實現分析
公平鎖和非公平鎖
ReentrantLock
的公平鎖和非公平鎖都委托了 AbstractQueuedSynchronizer#acquire
去請求獲取。
1 public final void acquire(int arg) { 2 if (!tryAcquire(arg) &&
3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 4 selfInterrupt(); 5 }
tryAcquire 是一個抽象方法,是公平與非公平的實現原理所在。
addWaiter 是將當前線程結點加入等待隊列之中。公平鎖在鎖釋放后會嚴格按照等到隊列去取后續值,而非公平鎖在對於新晉線程有很大優勢。
acquireQueued 在多次循環中嘗試獲取到鎖或者將當前線程阻塞。
selfInterrupt 如果線程在阻塞期間發生了中斷,調用 Thread.currentThread().interrupt() 中斷當前線程。
ReentrantLock 對線程的阻塞是基於 LockSupport.park(this); (見 AbstractQueuedSynchronizer#parkAndCheckInterrupt)。 先決條件是當前節點有限次嘗試獲取鎖失敗。
公平鎖和非公平鎖在說的獲取上都使用到了 volatile 關鍵字修飾的state字段, 這是保證多線程環境下鎖的獲取與否的核心。
但是當並發情況下多個線程都讀取到 state == 0時,則必須用到CAS技術,一門CPU的原子鎖技術,可通過CPU對共享變量加鎖的形式,實現數據變更的原子操作。
volatile 和 CAS的結合是並發搶占的關鍵。
公平鎖FairSync
公平鎖的實現機理在於每次有線程來搶占鎖的時候,都會檢查一遍有沒有等待隊列,如果有, 當前線程會執行如下步驟:
1 if (!hasQueuedPredecessors() &&
2 compareAndSetState(0, acquires)) { 3 setExclusiveOwnerThread(current); 4 return true; 5 }
其中hasQueuedPredecessors
是用於檢查是否有等待隊列的。
1 public final boolean hasQueuedPredecessors() { 2 Node t = tail; // Read fields in reverse initialization order
3 Node h = head; 4 Node s; 5 return h != t && ((s = h.next) == null || s.thread !=
6 Thread.currentThread()); 7 }
非公平鎖NonfairSync
非公平鎖在實現的時候多次強調隨機搶占:
1 if (c == 0) { 2 if (compareAndSetState(0, acquires)) { 3 setExclusiveOwnerThread(current); 4 return true; 5 } 6 }
與公平鎖的區別在於新晉獲取鎖的進程會有多次機會去搶占鎖。如果被加入了等待隊列后則跟公平鎖沒有區別。
ReentrantLock鎖的釋放
ReentrantLock鎖的釋放是逐級釋放的,也就是說在 可重入性 場景中,必須要等到場景內所有的加鎖的方法都釋放了鎖, 當前線程持有的鎖才會被釋放!
釋放的方式很簡單, state字段減一即可:
1 protected final boolean tryRelease(int releases) { 2 // releases = 1 int c = getState() - releases;
3 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); 4 boolean free = false; 5 if (c == 0) { 6 free = true; 7 setExclusiveOwnerThread(null); 8 } 9 setState(c); 10 return free; 11 }
ReentrantLock等待隊列中元素的喚醒
當當前擁有鎖的線程釋放鎖之后, 且非公平鎖無線程搶占,就開始線程喚醒的流程。
通過tryRelease
釋放鎖成功,調用LockSupport.unpark(s.thread);
終止線程阻塞。
見代碼:
1 private void unparkSuccessor(Node node) { 2 // 強行回寫將被喚醒線程的狀態
3 int ws = node.waitStatus; 4 if (ws < 0) 5 compareAndSetWaitStatus(node, ws, 0); 6 Node s = node.next; 7 // s為h的下一個Node, 一般情況下都是非Null的
8 if (s == null || s.waitStatus > 0) { 9 s = null; 10 // 否則按照FIFO原則尋找最先入隊列的並且沒有被Cancel的Node
11 for (Node t = tail; t != null && t != node; t = t.prev){ 12 if (t.waitStatus <= 0) 13 s = t; 14 // 再喚醒它
15 if (s != null) 16 LockSupport.unpark(s.thread); 17 } 18 } 19 }
ReentrantLock內存可見性分析
針對如下代碼:
1 try { 2 lock.lock(); 3 i ++; 4 } finally { 5 lock.unlock(); 6 }
可以發現哪怕在不使用 volatile
關鍵字修飾元素i
的時候, 這里的i
也是沒有並發問題的。
CAS和volatile, Java並發的基石
volatile 是Java語言的關鍵字, 功能是保證被修飾的元素(共享變量):
任何進程在讀取的時候,都會清空本進程里面持有的共享變量的值,強制從主存里面獲取;
任何進程在寫入完畢的時候,都會強制將共享變量的值寫會主存。
volatile 會干預指令重排。
volatile 實現了JMM規范的 happen-before 原則。
在多核多線程CPU環境下, CPU為了提升指令執行速度,在保證程序語義正確的前提下,允許編譯器對指令進行重排序。也就是說這種指令重排序對於上層代碼是感知不到的,我們稱之為 processor ordering.
JMM 允許編譯器在指令重排上自由發揮,除非程序員通過 volatile等 顯式干預這種重排機制,建立起同步機制,保證多線程代碼正確運行。見文章:Java並發:volatile內存可見性和指令重排。
當多個線程之間有互相的數據依賴的之后, 就必須顯式的干預這個指令重排機制。
CAS是CPU提供的一門技術。在單核單線程處理器上,所有的指令允許都是順序操作;但是在多核多線程處理器上,多線程訪問同一個共享變量的時候,可能存在並發問題。
使用CAS技術可以鎖定住元素的值。Intel開發文檔, 第八章
編譯器在將線程持有的值與被鎖定的值進行比較,相同則更新為更新的值。
CAS同樣遵循JMM規范的 happen-before 原則。
看JAVA CAS原理深度分析博客
公平鎖和非公平鎖在說的獲取上都使用到了 volatile 關鍵字修飾的state字段, 這是保證多線程環境下鎖的獲取與否的核心。
但是當並發情況下多個線程都讀取到 state == 0時,則必須用到CAS技術,一門CPU的原子鎖技術,可通過CPU對共享變量加鎖的形式,實現數據變更的原子操作。
volatile 和 CAS的結合是並發搶占的關鍵。
JSR-133編譯器編寫手冊
JMM規范經歷了多代迭代, JSR-133為較為通用的一版規范。
編譯器編寫手冊文檔見: The JSR-133 Cookbook for Compiler Writers (非官方指南)
上面小章節描述到了 volatile
可以避免掉的指令重排, 那它怎么避免的呢?
在內存的讀寫過程中, 無非 讀/寫 兩者操作的四種組合:
- LoadStore
- LoadLoad
- StoreStore
- StoreLoad
volatile關鍵字通過提供“內存屏障”的方式來防止指令被重排序,為了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。而大多數的處理器都支持內存屏障的指令。
volatile讀操作的后面插入一個LoadLoad屏障。
volatile寫操作的后面插入一個StoreLoad屏障。
那這個StoreLoad /LoadLoad有什么用處呢? 見 Intel開發文檔, 第八章。 簡單的說StoreLoad就是觸發后續指令中的線程緩存回寫到內存; 而LoadLoad會觸發線程重新從主存里面讀數據進行處理。

Synchronization mechanisms in multiple-processor systems may depend upon a strong memory-ordering model. Here, a program can use a locking instruction such as the XCHG instruction or the LOCK prefix to ensure that a read-modify-write operation on memory is carried out atomically. Locking operations typically operate like I/O operations in that they wait for all previous instructions to complete and for all buffered writes to drain to memory.
ReentrantLock內存可見性
在上述博客中的: ReentrantLock鎖的實現分析#公平鎖和非公平鎖 中講到:ReentrantLock 通過 volatile 和 CAS 的搭配實現鎖的功能。
順帶的, volatile 關鍵字修飾的 state 字段讀和后續的鎖釋放中的 state 字段寫, 共同組成了保證ReentrantLock內存可見性的內存屏障。 此屏障保證了ReentrantLock的內存可見性
CAS的類似volatile內存屏障原理
參見文章 深入理解Java內存模型(五)——鎖
如下文檔部分摘錄
volatile是通過在Java編譯時,添加字節碼來實現內存屏障功能。
CAS通過本地JNI調用,Java代碼為 Unsafe.java, 層次調用為:unsafe.cpp > atomic.cpp > atomicwindowsx86.inline.hpp。調用的代碼如是:

#define LOCK_IF_MP(mp) __asm cmp mp, 0 \ __asm je L0 \ __asm _emit 0xF0 \ __asm L0: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange
int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } }
如上面源代碼所示,程序會根據當前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不需要lock前綴提供的內存屏障效果)。
intel的手冊對lock前綴的說明如下:
1):確保對內存的讀-改-寫操作原子執行。在Pentium及Pentium之前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其他處理器暫時無法通過總線訪問內存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上做了一個很有意義的優化:如果要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處於獨占或以修改狀態),並且該內存區域被完全包含在單個緩存行(cache line)中,那么處理器將直接執行該指令。由於在指令執行期間該緩存行會一直被鎖定,其它處理器無法讀/寫該指令要訪問的內存區域,因此能保證指令執行的原子性。這個操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執行開銷,但是當多處理器之間的競爭程度很高或者指令訪問的內存地址未對齊時,仍然會鎖住總線。
2):禁止該指令與之前和之后的讀和寫指令重排序。
3):把寫緩沖區中的所有數據刷新到內存中。
上面的第2點和第3點所具有的內存屏障效果,足以同時實現volatile讀和volatile寫的內存語義。