Java語言中有許多原生線程安全的數據結構,好比ArrayBlockingQueue
、CopyOnWriteArrayList
、LinkedBlockingQueue
,它們線程安全的實現方式並不是經過synchronized
關鍵字,而是經過java.util.concurrent.locks.ReentrantLock
來實現。 恰好對這個很感興趣, 所以寫一篇博客詳細分析此 「可重入鎖實現原理」。
ReentrantLock
的實現是基於其內部類FairSync
(公平鎖)和NonFairSync
(非公平鎖)實現的。 其可重入性是基於Thread.currentThread()
實現的: 若是當前線程已經得到了執行序列中的鎖, 那執行序列以後的全部方法均可以得到這個鎖。html
公平鎖:java
Node
和狀態state
的volatile
關鍵字。sum.misc.Unsafe.compareAndSet
的原子操做(見附錄)。非公平鎖:node
ReentrantLock
鎖都不會使得線程中斷,除非開發者本身設置了中斷位。
ReentrantLock
獲取鎖裏面有看似自旋的代碼,可是它不是自旋鎖。
ReentrantLock
公平與非公平鎖都是屬於排它鎖。程序員
這裏有一篇對鎖介紹甚爲詳細的文章 朱小廝的博客-Java中的鎖.windows
參考這篇文章: Java內置鎖synchronized的可重入性。緩存
java線程是基於「每線程(per-thread)」,而不是基於「每調用(per-invocation)」的(java中線程得到對象鎖的操做是以每線程爲粒度的,per-invocation互斥體得到對象鎖的操做是以每調用做爲粒度的)安全
前言裏面提到,ReentrantLock
重入性是基於Thread.currentThread()
實現的: 若是當前線程已經得到了鎖, 那該線程下的全部方法均可以得到這個鎖。ReentrantLock
的鎖依賴只有 NonfairSync
和FairSync
兩個實現類, 他們的鎖獲取方式大同小異。數據結構
可重入性的實現基於下面代碼片斷的
else if
語句多線程
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { ... // 嘗試獲取鎖成功 } else if (current == getExclusiveOwnerThread()) { // 是當前線程,直接獲取到鎖。實現可重入性。 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
此處有兩個值須要關心:併發
/** * The current owner of exclusive mode synchronization. * 持有該鎖的當前線程 */ private transient Thread exclusiveOwnerThread; -----------------兩個值不在同一個類---------------- /** * The synchronization state. * 0: 初始狀態-無任何線程獲得了鎖 * > 0: 被線程持有, 具體值表示被當前線程持有的執行次數 * * 這個字段在解鎖的時候也須要用到。 * 注意這個字段的修飾詞: volatile */ private volatile int state;
ReentrantLock
的公平鎖和非公平鎖都委託了 AbstractQueuedSynchronizer#acquire
去請求獲取。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire
是一個抽象方法,是公平與非公平的實現原理所在。addWaiter
是將當前線程結點加入等待隊列之中。公平鎖在鎖釋放後會嚴格按照等到隊列去取後續值,而非公平鎖在對於新晉線程有很大優點。acquireQueued
在屢次循環中嘗試獲取到鎖或者將當前線程阻塞。selfInterrupt
若是線程在阻塞期間發生了中斷,調用 Thread.currentThread().interrupt()
中斷當前線程。
ReentrantLock
對線程的阻塞是基於LockSupport.park(this);
(見AbstractQueuedSynchronizer#parkAndCheckInterrupt
)。 先決條件是當前節點有限次嘗試獲取鎖失敗。
公平鎖和非公平鎖在說的獲取上都使用到了 volatile
關鍵字修飾的state
字段, 這是保證多線程環境下鎖的獲取與否的核心。
可是當併發狀況下多個線程都讀取到 state == 0
時,則必須用到CAS技術,一門CPU的原子鎖技術,可經過CPU對共享變量加鎖的形式,實現數據變動的原子操做。
volatile 和 CAS的結合是併發搶佔的關鍵。
公平鎖的實現機理在於每次有線程來搶佔鎖的時候,都會檢查一遍有沒有等待隊列,若是有, 當前線程會執行以下步驟:
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; }
其中hasQueuedPredecessors
是用於檢查是否有等待隊列的。
public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
非公平鎖在實現的時候屢次強調隨機搶佔:
if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } }
與公平鎖的區別在於新晉獲取鎖的進程會有屢次機會去搶佔鎖。若是被加入了等待隊列後則跟公平鎖沒有區別。
ReentrantLock鎖的釋放是逐級釋放的,也就是說在 可重入性 場景中,必需要等到場景內全部的加鎖的方法都釋放了鎖, 當前線程持有的鎖纔會被釋放!
釋放的方式很簡單, state字段減一便可:
protected final boolean tryRelease(int releases) { // releases = 1 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
噹噹前擁有鎖的線程釋放鎖以後, 且非公平鎖無線程搶佔,就開始線程喚醒的流程。
經過tryRelease
釋放鎖成功,調用LockSupport.unpark(s.thread);
終止線程阻塞。
見代碼:
private void unparkSuccessor(Node node) { // 強行回寫將被喚醒線程的狀態 int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; // s爲h的下一個Node, 通常狀況下都是非Null的 if (s == null || s.waitStatus > 0) { s = null; // 不然按照FIFO原則尋找最早入隊列的而且沒有被Cancel的Node for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } // 再喚醒它 if (s != null) LockSupport.unpark(s.thread); }
針對以下代碼:
try { lock.lock(); i ++; } finally { lock.unlock(); }
能夠發現哪怕在不使用 volatile
關鍵字修飾元素i
的時候, 這裏的i
也是沒有併發問題的。
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的結合是併發搶佔的關鍵。
JMM規範經歷了多代迭代, JSR-133爲較爲通用的一版規範。
編譯器編寫手冊文檔見: The JSR-133 Cookbook for Compiler Writers (非官方指南)
上面小章節描述到了 volatile
能夠避免掉的指令重排, 那它怎麼避免的呢?
在內存的讀寫過程當中, 無非 讀/寫 二者操做的四種組合:
volatile關鍵字經過提供「內存屏障」的方式來防止指令被重排序,爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。而大多數的處理器都支持內存屏障的指令。
那這個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 經過 volatile
和 CAS
的搭配實現鎖的功能。
順帶的, volatile
關鍵字修飾的 state
字段讀和後續的鎖釋放中的 state
字段寫, 共同組成了保證ReentrantLock內存可見性的內存屏障。 此屏障保證了ReentrantLock的內存可見性
參見文章 深刻理解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前綴的說明以下:
上面的第2點和第3點所具備的內存屏障效果,足以同時實現volatile讀和volatile寫的內存語義。
原文:https://blog.csdn.net/qyp199312/article/details/70598480