Java中的公平鎖和非公平鎖實現詳解

 

前言html

Java語言中有許多原生線程安全的數據結構,好比ArrayBlockingQueueCopyOnWriteArrayListLinkedBlockingQueue,它們線程安全的實現方式並不是經過synchronized關鍵字,而是經過java.util.concurrent.locks.ReentrantLock來實現。 恰好對這個很感興趣, 所以寫一篇博客詳細分析此 「可重入鎖實現原理」。
ReentrantLock的實現是基於其內部類FairSync(公平鎖)和NonFairSync(非公平鎖)實現的。 其可重入性是基於Thread.currentThread()實現的: 若是當前線程已經得到了執行序列中的鎖, 那執行序列以後的全部方法均可以得到這個鎖。java

公平鎖:node

公平和非公平鎖的隊列都基於鎖內部維護的一個雙向鏈表,表結點Node的值就是每個請求當前鎖的線程。公平鎖則在於每次都是依次從隊首取值。
鎖的實現方式是基於以下幾點:
表結點Node和狀態statevolatile關鍵字。
sum.misc.Unsafe.compareAndSet的原子操做(見附錄)。程序員

非公平鎖:windows

在等待鎖的過程當中, 若是有任意新的線程妄圖獲取鎖,都是有很大的概率直接獲取到鎖的。緩存

ReentrantLock鎖都不會使得線程中斷,除非開發者本身設置了中斷位。
ReentrantLock獲取鎖裏面有看似自旋的代碼,可是它不是自旋鎖。
ReentrantLock公平與非公平鎖都是屬於排它鎖。

ReentrantLock的可重入性分析安全

這裏有一篇對鎖介紹甚爲詳細的文章 朱小廝的博客-Java中的鎖.
synchronized的可重入性數據結構

參考這篇文章: Java內置鎖synchronized的可重入性多線程

java線程是基於「每線程(per-thread)」,而不是基於「每調用(per-invocation)」的(java中線程得到對象鎖的操做是以每線程爲粒度的,per-invocation互斥體得到對象鎖的操做是以每調用做爲粒度的)併發

ReentrantLock的可重入性

前言裏面提到,ReentrantLock重入性是基於Thread.currentThread()實現的: 若是當前線程已經得到了鎖, 那該線程下的全部方法均可以得到這個鎖。ReentrantLock的鎖依賴只有 NonfairSyncFairSync兩個實現類, 他們的鎖獲取方式大同小異。

可重入性的實現基於下面代碼片斷的 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對共享變量加鎖的形式,實現數據變動的原子操做。
volatileCAS的結合是併發搶佔的關鍵。

公平鎖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.
View Code

ReentrantLock內存可見性

在上述博客中的: ReentrantLock鎖的實現分析#公平鎖和非公平鎖 中講到:ReentrantLock 經過 volatileCAS 的搭配實現鎖的功能。

順帶的, 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 } }
View Code

 

如上面源代碼所示,程序會根據當前處理器的類型來決定是否爲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寫的內存語義。

相關文章
相關標籤/搜索