Java併發系列[5]----ReentrantLock源碼分析

在Java5.0以前,協調對共享對象的訪問可使用的機制只有synchronized和volatile。咱們知道synchronized關鍵字實現了內置鎖,而volatile關鍵字保證了多線程的內存可見性。在大多數狀況下,這些機制都能很好地完成工做,但卻沒法實現一些更高級的功能,例如,沒法中斷一個正在等待獲取鎖的線程,沒法實現限定時間的獲取鎖機制,沒法實現非阻塞結構的加鎖規則等。而這些更靈活的加鎖機制一般都可以提供更好的活躍性或性能。所以,在Java5.0中增長了一種新的機制:ReentrantLock。ReentrantLock類實現了Lock接口,並提供了與synchronized相同的互斥性和內存可見性,它的底層是經過AQS來實現多線程同步的。與內置鎖相比ReentrantLock不只提供了更豐富的加鎖機制,並且在性能上也不遜色於內置鎖(在之前的版本中甚至優於內置鎖)。說了ReentrantLock這麼多的優勢,那麼下面咱們就來揭開它的源碼看看它的具體實現。html

1.synchronized關鍵字的介紹java

Java提供了內置鎖來支持多線程的同步,JVM根據synchronized關鍵字來標識同步代碼塊,當線程進入同步代碼塊時會自動獲取鎖,退出同步代碼塊時會自動釋放鎖,一個線程得到鎖後其餘線程將會被阻塞。每一個Java對象均可以用作一個實現同步的鎖,synchronized關鍵字能夠用來修飾對象方法,靜態方法和代碼塊,當修飾對象方法和靜態方法時鎖分別是方法所在的對象和Class對象,當修飾代碼塊時需提供額外的對象做爲鎖。每一個Java對象之因此能夠做爲鎖,是由於在對象頭中關聯了一個monitor對象(管程),線程進入同步代碼塊時會自動持有monitor對象,退出時會自動釋放monitor對象,當monitor對象被持有時其餘線程將會被阻塞。固然這些同步操做都由JVM底層幫你實現了,但以synchronized關鍵字修飾的方法和代碼塊在底層實現上仍是有些區別的。synchronized關鍵字修飾的方法是隱式同步的,即無需經過字節碼指令來控制的,JVM能夠根據方法表中的ACC_SYNCHRONIZED訪問標誌來區分一個方法是不是同步方法;而synchronized關鍵字修飾的代碼塊是顯式同步的,它是經過monitorenter和monitorexit字節碼指令來控制線程對管程的持有和釋放。monitor對象內部持有_count字段,_count等於0表示管程未被持有,_count大於0表示管程已被持有,每次持有線程重入時_count都會加1,每次持有線程退出時_count都會減1,這就是內置鎖重入性的實現原理。另外,monitor對象內部還有兩條隊列_EntryList和_WaitSet,對應着AQS的同步隊列和條件隊列,當線程獲取鎖失敗時會到_EntryList中阻塞,當調用鎖對象的wait方法時線程將會進入_WaitSet中等待,這是內置鎖的線程同步和條件等待的實現原理。安全

2.ReentrantLock和Synchronized的比較多線程

synchronized關鍵字是Java提供的內置鎖機制,其同步操做由底層JVM實現,而ReentrantLock是java.util.concurrent包提供的顯式鎖,其同步操做由AQS同步器提供支持。ReentrantLock在加鎖和內存上提供的語義與內置鎖相同,此外它還提供了一些其餘功能,包括定時的鎖等待,可中斷的鎖等待,公平鎖,以及實現非塊結構的加鎖。另外,在早期的JDK版本中ReentrantLock在性能上還佔有必定的優點,既然ReentrantLock擁有這麼多優點,爲何還要使用synchronized關鍵字呢?事實上確實有許多人使用ReentrantLock來替代synchronized關鍵字的加鎖操做。可是內置鎖仍然有它特有的優點,內置鎖爲許多開發人員所熟悉,使用方式也更加的簡潔緊湊,由於顯式鎖必須手動在finally塊中調用unlock,因此使用內置鎖相對來講會更加安全些。同時將來更加可能會去提高synchronized而不是ReentrantLock的性能。由於synchronized是JVM的內置屬性,它能執行一些優化,例如對線程封閉的鎖對象的鎖消除優化,經過增長鎖的粒度來消除內置鎖的同步,而若是經過基於類庫的鎖來實現這些功能,則可能性不大。因此當須要一些高級功能時才應該使用ReentrantLock,這些功能包括:可定時的,可輪詢的與可中斷的鎖獲取操做,公平隊列,以及非塊結構的鎖。不然,仍是應該優先使用synchronized。併發

3.獲取鎖和釋放鎖的操做源碼分析

咱們首先來看一下使用ReentrantLock加鎖的示例代碼。性能

 1 public void doSomething() {
 2     //默認是獲取一個非公平鎖
 3     ReentrantLock lock = new ReentrantLock();
 4     try{
 5         //執行前先加鎖
 6         lock.lock();   
 7         //執行操做...
 8     }finally{
 9         //最後釋放鎖
10         lock.unlock();
11     }
12 }

如下是獲取鎖和釋放鎖這兩個操做的API。優化

1 //獲取鎖的操做
2 public void lock() {
3     sync.lock();
4 }
5 //釋放鎖的操做
6 public void unlock() {
7     sync.release(1);
8 }

能夠看到獲取鎖和釋放鎖的操做分別委託給Sync對象的lock方法和release方法。ui

 1 public class ReentrantLock implements Lock, java.io.Serializable {
 2     
 3     private final Sync sync;
 4 
 5     abstract static class Sync extends AbstractQueuedSynchronizer {
 6         abstract void lock();
 7     }
 8     
 9     //實現非公平鎖的同步器
10     static final class NonfairSync extends Sync {
11         final void lock() {
12             ...
13         }
14     }
15     
16     //實現公平鎖的同步器
17     static final class FairSync extends Sync {
18         final void lock() {
19             ...
20         }
21     }
22 }

每一個ReentrantLock對象都持有一個Sync類型的引用,這個Sync類是一個抽象內部類它繼承自AbstractQueuedSynchronizer,它裏面的lock方法是一個抽象方法。ReentrantLock的成員變量sync是在構造時賦值的,下面咱們看看ReentrantLock的兩個構造方法都作了些什麼?spa

1 //默認無參構造器
2 public ReentrantLock() {
3     sync = new NonfairSync();
4 }
5 
6 //有參構造器
7 public ReentrantLock(boolean fair) {
8     sync = fair ? new FairSync() : new NonfairSync();
9 }

調用默認無參構造器會將NonfairSync實例賦值給sync,此時鎖是非公平鎖。有參構造器容許經過參數來指定是將FairSync實例仍是NonfairSync實例賦值給sync。NonfairSync和FairSync都是繼承自Sync類並重寫了lock()方法,因此公平鎖和非公平鎖在獲取鎖的方式上有些區別,這個咱們下面會講到。再來看看釋放鎖的操做,每次調用unlock()方法都只是去執行sync.release(1)操做,這步操做會調用AbstractQueuedSynchronizer類的release()方法,咱們再來回顧一下。

 1 //釋放鎖的操做(獨佔模式)
 2 public final boolean release(int arg) {
 3     //撥動密碼鎖, 看看是否可以開鎖
 4     if (tryRelease(arg)) {
 5         //獲取head結點
 6         Node h = head;
 7         //若是head結點不爲空而且等待狀態不等於0就去喚醒後繼結點
 8         if (h != null && h.waitStatus != 0) {
 9             //喚醒後繼結點
10             unparkSuccessor(h);
11         }
12         return true;
13     }
14     return false;
15 }

這個release方法是AQS提供的釋放鎖操做的API,它首先會去調用tryRelease方法去嘗試獲取鎖,tryRelease方法是抽象方法,它的實現邏輯在子類Sync裏面。

 1 //嘗試釋放鎖
 2 protected final boolean tryRelease(int releases) {
 3     int c = getState() - releases;
 4     //若是持有鎖的線程不是當前線程就拋出異常
 5     if (Thread.currentThread() != getExclusiveOwnerThread()) {
 6         throw new IllegalMonitorStateException();
 7     }
 8     boolean free = false;
 9     //若是同步狀態爲0則代表鎖被釋放
10     if (c == 0) {
11         //設置鎖被釋放的標誌爲真
12         free = true;
13         //設置佔用線程爲空
14         setExclusiveOwnerThread(null);
15     }
16     setState(c);
17     return free;
18 }

這個tryRelease方法首先會獲取當前同步狀態,並將當前同步狀態減去傳入的參數值獲得新的同步狀態,而後判斷新的同步狀態是否等於0,若是等於0則代表當前鎖被釋放,而後先將鎖的釋放狀態置爲真,再將當前佔有鎖的線程清空,最後調用setState方法設置新的同步狀態並返回鎖的釋放狀態。

4.公平鎖和非公平鎖

咱們知道ReentrantLock是公平鎖仍是非公平鎖是基於sync指向的是哪一個具體實例。在構造時會爲成員變量sync賦值,若是賦值爲NonfairSync實例則代表是非公平鎖,若是賦值爲FairSync實例則代表爲公平鎖。若是是公平鎖,線程將按照它們發出請求的順序來得到鎖,但在非公平鎖上,則容許插隊行爲:當一個線程請求非公平的鎖時,若是在發出請求的同時該鎖的狀態變爲可用,那麼這個線程將跳過隊列中全部等待的線程直接得到這個鎖。下面咱們先看看非公平鎖的獲取方式。

 1 //非公平同步器
 2 static final class NonfairSync extends Sync {
 3     //實現父類的抽象獲取鎖的方法
 4     final void lock() {
 5         //使用CAS方式設置同步狀態
 6         if (compareAndSetState(0, 1)) {
 7             //若是設置成功則代表鎖沒被佔用
 8             setExclusiveOwnerThread(Thread.currentThread());
 9         } else {
10             //不然代表鎖已經被佔用, 調用acquire讓線程去同步隊列排隊獲取
11             acquire(1);
12         }
13     }
14     //嘗試獲取鎖的方法
15     protected final boolean tryAcquire(int acquires) {
16         return nonfairTryAcquire(acquires);
17     }
18 }
19 
20 //以不可中斷模式獲取鎖(獨佔模式)
21 public final void acquire(int arg) {
22     if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
23         selfInterrupt();
24     }
25 }

能夠看到在非公平鎖的lock方法中,線程第一步就會以CAS方式將同步狀態的值從0改成1。其實這步操做就等於去嘗試獲取鎖,若是更改爲功則代表線程剛來就獲取了鎖,而沒必要再去同步隊列裏面排隊了。若是更改失敗則代表線程剛來時鎖還未被釋放,因此接下來就調用acquire方法。咱們知道這個acquire方法是繼承自AbstractQueuedSynchronizer的方法,如今再來回顧一下該方法,線程進入acquire方法後首先去調用tryAcquire方法嘗試去獲取鎖,因爲NonfairSync覆蓋了tryAcquire方法,並在方法中調用了父類Sync的nonfairTryAcquire方法,因此這裏會調用到nonfairTryAcquire方法去嘗試獲取鎖。咱們看看這個方法具體作了些什麼。

 1 //非公平的獲取鎖
 2 final boolean nonfairTryAcquire(int acquires) {
 3     //獲取當前線程
 4     final Thread current = Thread.currentThread();
 5     //獲取當前同步狀態
 6     int c = getState();
 7     //若是同步狀態爲0則代表鎖沒有被佔用
 8     if (c == 0) {
 9         //使用CAS更新同步狀態
10         if (compareAndSetState(0, acquires)) {
11             //設置目前佔用鎖的線程
12             setExclusiveOwnerThread(current);
13             return true;
14         }
15     //不然的話就判斷持有鎖的是不是當前線程
16     }else if (current == getExclusiveOwnerThread()) {
17         //若是鎖是被當前線程持有的, 就直接修改當前同步狀態
18         int nextc = c + acquires;
19         if (nextc < 0) {
20             throw new Error("Maximum lock count exceeded");
21         }
22         setState(nextc);
23         return true;
24     }
25     //若是持有鎖的不是當前線程則返回失敗標誌
26     return false;
27 }

nonfairTryAcquire方法是Sync的方法,咱們能夠看到線程進入此方法後首先去獲取同步狀態,若是同步狀態爲0就使用CAS操做更改同步狀態,其實這又是獲取了一遍鎖。若是同步狀態不爲0代表鎖被佔用,此時會先去判斷持有鎖的線程是不是當前線程,若是是的話就將同步狀態加1,不然的話此次嘗試獲取鎖的操做宣告失敗。因而會調用addWaiter方法將線程添加到同步隊列。綜上來看,在非公平鎖的模式下一個線程在進入同步隊列以前會嘗試獲取兩遍鎖,若是獲取成功則不進入同步隊列排隊,不然才進入同步隊列排隊。接下來咱們看看公平鎖的獲取方式。

 1 //實現公平鎖的同步器
 2 static final class FairSync extends Sync {
 3     //實現父類的抽象獲取鎖的方法
 4     final void lock() {
 5         //調用acquire讓線程去同步隊列排隊獲取
 6         acquire(1);
 7     }
 8     //嘗試獲取鎖的方法
 9     protected final boolean tryAcquire(int acquires) {
10         //獲取當前線程
11         final Thread current = Thread.currentThread();
12         //獲取當前同步狀態
13         int c = getState();
14         //若是同步狀態0則表示鎖沒被佔用
15         if (c == 0) {
16             //判斷同步隊列是否有前繼結點
17             if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
18                 //若是沒有前繼結點且設置同步狀態成功就表示獲取鎖成功
19                 setExclusiveOwnerThread(current);
20                 return true;
21             }
22         //不然判斷是不是當前線程持有鎖
23         }else if (current == getExclusiveOwnerThread()) {
24             //若是是當前線程持有鎖就直接修改同步狀態
25             int nextc = c + acquires;
26             if (nextc < 0) {
27                 throw new Error("Maximum lock count exceeded");
28             }
29             setState(nextc);
30             return true;
31         }
32         //若是不是當前線程持有鎖則獲取失敗
33         return false;
34     }
35 }

調用公平鎖的lock方法時會直接調用acquire方法。一樣的,acquire方法首先會調用FairSync重寫的tryAcquire方法來嘗試獲取鎖。在該方法中也是首先獲取同步狀態的值,若是同步狀態爲0則代表此時鎖恰好被釋放,這時和非公平鎖不一樣的是它會先去調用hasQueuedPredecessors方法查詢同步隊列中是否有人在排隊,若是沒人在排隊纔會去修改同步狀態的值,能夠看到公平鎖在這裏採起禮讓的方式而不是本身立刻去獲取鎖。除了這一步和非公平鎖不同以外,其餘的操做都是同樣的。綜上所述,能夠看到公平鎖在進入同步隊列以前只檢查了一遍鎖的狀態,即便是發現了鎖是開的也不會本身立刻去獲取,而是先讓同步隊列中的線程先獲取,因此能夠保證在公平鎖下全部線程獲取鎖的順序都是先來後到的,這也保證了獲取鎖的公平性。
那麼咱們爲何不但願全部鎖都是公平的呢?畢竟公平是一種好的行爲,而不公平是一種很差的行爲。因爲線程的掛起和喚醒操做存在較大的開銷而影響系統性能,特別是在競爭激烈的狀況下公平鎖將致使線程頻繁的掛起和喚醒操做,而非公平鎖能夠減小這樣的操做,因此在性能上將會優於公平鎖。另外,因爲大部分線程使用鎖的時間都是很是短暫的,而線程的喚醒操做會存在延時狀況,有可能在A線程被喚醒期間B線程立刻獲取了鎖並使用完釋放了鎖,這就致使了共贏的局面,A線程獲取鎖的時刻並無推遲,但B線程提早使用了鎖,而且吞吐量也得到了提升。

5.條件隊列的實現機制

內置條件隊列存在一些缺陷,每一個內置鎖都只能有一個相關聯的條件隊列,這致使多個線程可能在同一個條件隊列上等待不一樣的條件謂詞,那麼每次調用notifyAll時都會將全部等待的線程喚醒,當線程醒來後發現並非本身等待的條件謂詞,轉而又會被掛起。這致使作了不少無用的線程喚醒和掛起操做,而這些操做將會大量浪費系統資源,下降系統的性能。若是想編寫一個帶有多個條件謂詞的併發對象,或者想得到除了條件隊列可見性以外的更多控制權,就須要使用顯式的Lock和Condition而不是內置鎖和條件隊列。一個Condition和一個Lock關聯在一塊兒,就像一個條件隊列和一個內置鎖相關聯同樣。要建立一個Condition,能夠在相關聯的Lock上調用Lock.newCondition方法。咱們先來看一個使用Condition的示例。

 1 public class BoundedBuffer {
 2 
 3     final Lock lock = new ReentrantLock();
 4     final Condition notFull = lock.newCondition();   //條件謂詞:notFull
 5     final Condition notEmpty = lock.newCondition();  //條件謂詞:notEmpty
 6     final Object[] items = new Object[100];
 7     int putptr, takeptr, count;
 8 
 9     //生產方法
10     public void put(Object x) throws InterruptedException {
11         lock.lock();
12         try {
13             while (count == items.length)
14                 notFull.await();  //隊列已滿, 線程在notFull隊列上等待
15             items[putptr] = x;
16             if (++putptr == items.length) putptr = 0;
17             ++count;
18             notEmpty.signal(); //生產成功, 喚醒notEmpty隊列的結點
19         } finally {
20             lock.unlock();
21         }
22     }
23 
24     //消費方法
25     public Object take() throws InterruptedException {
26         lock.lock();
27         try {
28             while (count == 0)
29                 notEmpty.await(); //隊列爲空, 線程在notEmpty隊列上等待
30             Object x = items[takeptr];
31             if (++takeptr == items.length) takeptr = 0;
32             --count;
33             notFull.signal();  //消費成功, 喚醒notFull隊列的結點
34             return x;
35         } finally {
36             lock.unlock();
37         }
38     }
39     
40 }

一個lock對象能夠產生多個條件隊列,這裏產生了兩個條件隊列notFull和notEmpty。當容器已滿時再調用put方法的線程須要進行阻塞,等待條件謂詞爲真(容器不滿)才醒來繼續執行;當容器爲空時再調用take方法的線程也須要阻塞,等待條件謂詞爲真(容器不空)才醒來繼續執行。這兩類線程是根據不一樣的條件謂詞進行等待的,因此它們會進入兩個不一樣的條件隊列中阻塞,等到合適時機再經過調用Condition對象上的API進行喚醒。下面是newCondition方法的實現代碼。

 1 //建立條件隊列
 2 public Condition newCondition() {
 3     return sync.newCondition();
 4 }
 5 
 6 abstract static class Sync extends AbstractQueuedSynchronizer {
 7     //新建Condition對象
 8     final ConditionObject newCondition() {
 9         return new ConditionObject();
10     }
11 }

ReentrantLock上的條件隊列的實現都是基於AbstractQueuedSynchronizer的,咱們在調用newCondition方法時所得到的Condition對象就是AQS的內部類ConditionObject的實例。全部對條件隊列的操做都是經過調用ConditionObject對外提供的API來完成的。有關於ConditionObject的具體實現你們能夠查閱個人這篇文章《Java併發系列[4]----AbstractQueuedSynchronizer源碼分析之條件隊列》,這裏就不重複贅述了。至此,咱們對ReentrantLock源碼的剖析也告一段落,但願閱讀本篇文章可以對讀者們理解並掌握ReentrantLock起到必定的幫助做用。

相關文章
相關標籤/搜索