深入Lock锁底层原理实现,手写一个可重入锁
synchronized與lock
lock是一個接口,而synchronized是在JVM層面實現(xiàn)的。synchronized釋放鎖有兩種方式:
獲取鎖的線程執(zhí)行完同步代碼,釋放鎖 。
線程執(zhí)行發(fā)生異常,jvm會讓線程釋放鎖。
lock鎖的釋放,出現(xiàn)異常時必須在finally中釋放鎖,不然容易造成線程死鎖。lock顯式獲取鎖和釋放鎖,提供超時獲取鎖、可中斷地獲取鎖。
synchronized是以隱式地獲取和釋放鎖,synchronized無法中斷一個正在等待獲取鎖的線程。
synchronized原始采用的是CPU悲觀鎖機制,即線程獲得的是獨占鎖。獨占鎖意味著其他線程只能依靠阻塞來等待線程釋放鎖。而在CPU轉(zhuǎn)換線程阻塞時會引起線程上下文切換,當有很多線程競爭鎖的時候,會引起CPU頻繁的上下文切換導致效率很低。
Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設(shè)沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。樂觀鎖實現(xiàn)的機制就是CAS操作。
具體的悲觀鎖和樂觀鎖的詳細介紹請參考這篇文章[淺談數(shù)據(jù)庫樂觀鎖、悲觀鎖]
在JDK5中增加了一個Lock接口實現(xiàn)類ReentrantLock.它不僅擁有和synchronized相同的并發(fā)性和內(nèi)存語義,還多了鎖投票,定時鎖,等候和中斷鎖等.它們的性能在不同的情況下會有不同。
在資源競爭不是很激烈的情況下,synchronized的性能要由于ReentrantLock,但是在資源競爭很激烈的情況下,synchronized的性能會下降得非常快,而ReentrantLock的性能基本保持不變.
接下來我們會進一步研究ReentrantLock的源代碼,會發(fā)現(xiàn)其中比較重要的獲得鎖的一個方法是compareAndSetState。
lock源碼
在閱讀源碼的成長的過程中,有很多人會遇到很多困難,一個是源碼太多,另一方面是源碼看不懂。在閱讀源碼方面,我提供一些個人的建議:
第一個是抓主舍次,看源碼的時候,很多人會發(fā)現(xiàn)源碼太長太多,看不下去,這就要求我們抓住哪些是核心的方法,哪些是次要的方法。當舍去次要方法,就會發(fā)現(xiàn)代碼精簡和很多,會大大提高我們閱讀源碼的信心。
第二個是不要死扣,有人看源碼會一行一行的死扣,當看到某一行看不懂,就一直停在那里死扣,知道看懂為止,其實很多時候,雖然看不懂代碼,但是可以從變量名和方法名知道該代碼的作用,java中都是見名知意的。
接下來進入閱讀lock的源碼部分,在lock的接口中,主要的方法如下:
public?interface?Lock?{//?加鎖void?lock();//?嘗試獲取鎖boolean?tryLock();boolean?tryLock(long?time,?TimeUnit?unit)?throws?InterruptedException;//?解鎖void?unlock(); }在lock接口的實現(xiàn)類中,最主要的就是ReentrantLock,來看看ReentrantLock中l(wèi)ock()方法的源碼:
????//?默認構(gòu)造方法,非公平鎖public?ReentrantLock()?{sync?=?new?NonfairSync();}//?構(gòu)造方法,公平鎖public?ReentrantLock(boolean?fair)?{sync?=?fair???new?FairSync()?:?new?NonfairSync();}//?加鎖public?void?lock()?{sync.lock();}在初始化lock實例對象的時候,可以提供一個boolean的參數(shù),也可以不提供該參數(shù)。提供該參數(shù)就是公平鎖,不提供該參數(shù)就是非公平鎖。
什么是非公平鎖和公平鎖呢?
非公平鎖就是不按照線程先來后到的時間順序進行競爭鎖,后到的線程也能夠獲取到鎖,公平鎖就是按照線程先來后到的順序進行獲取鎖,后到的線程只能等前面的線程都獲取鎖完畢才執(zhí)行獲取鎖的操作,執(zhí)行有序。
我們來看看lock()這個方法,這個有區(qū)分公平鎖和非公平鎖,這個兩者的實現(xiàn)不同,先來看看公平鎖,源碼如下:
//?直接調(diào)用?acquire(1) final?void?lock()?{acquire(1);}我們來看看acquire(1)的源碼如下:
????public?final?void?acquire(int?arg)?{if?(!tryAcquire(arg)?&&acquireQueued(addWaiter(Node.EXCLUSIVE),?arg))selfInterrupt();}這里的判斷條件主要做兩件事:
通關(guān)過該方法tryAcquire(arg)嘗試的獲取鎖
若是沒有獲取到鎖,通過該方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg)就將當前的線程加入到存儲等待線程的隊列中。
其中tryAcquire(arg)是嘗試獲取鎖,這個方法是公平鎖的核心之一,它的源碼如下:
protected?final?boolean?tryAcquire(int?acquires)?{//?獲取當前線程?final?Thread?current?=?Thread.currentThread();//?獲取當前線程擁有著的狀態(tài)int?c?=?getState();//?若為0,說明當前線程擁有著已經(jīng)釋放鎖if?(c?==?0)?{//?判斷線程隊列中是否有,排在前面的線程等待著鎖,若是沒有設(shè)置線程的狀態(tài)為1。if?(!hasQueuedPredecessors()?&&compareAndSetState(0,?acquires))?{//?設(shè)置線程的擁有著為當前線程setExclusiveOwnerThread(current);return?true;}//?若是當前的線程的鎖的擁有者就是當前線程,可重入鎖}?else?if?(current?==?getExclusiveOwnerThread())?{//?執(zhí)行狀態(tài)值+1int?nextc?=?c?+?acquires;if?(nextc?<?0)throw?new?Error("Maximum?lock?count?exceeded");//?設(shè)置status的值為nextcsetState(nextc);return?true;}return?false;}在tryAcquire()方法中,主要是做了以下幾件事:
判斷當前線程的鎖的擁有者的狀態(tài)值是否為0,若為0,通過該方法hasQueuedPredecessors()再判斷等待線程隊列中,是否存在排在前面的線程。
若是沒有通過該方法?compareAndSetState(0, acquires)設(shè)置當前的線程狀態(tài)為1。
將線程擁有者設(shè)為當前線程setExclusiveOwnerThread(current)
若是當前線程的鎖的擁有者的狀態(tài)值不為0,說明當前的鎖已經(jīng)被占用,通過current == getExclusiveOwnerThread()判斷鎖的擁有者的線程,是否為當前線程,實現(xiàn)鎖的可重入。
若是當前線程將線程的狀態(tài)值+1,并更新狀態(tài)值。
公平鎖的tryAcquire(),實現(xiàn)的原理圖如下:
我們來看看acquireQueued()方法,該方法是將線程加入等待的線程隊列中,源碼如下:
final?boolean?acquireQueued(final?Node?node,?int?arg)?{boolean?failed?=?true;try?{boolean?interrupted?=?false;//?死循環(huán)處理for?(;;)?{//?獲取前置線程節(jié)點final?Node?p?=?node.predecessor();//?這里又嘗試的去獲取鎖if?(p?==?head?&&?tryAcquire(arg))?{setHead(node);p.next?=?null;?//?help?GCfailed?=?false;//?直接return??interruptedreturn?interrupted;}//?在獲取鎖失敗后,應(yīng)該將線程Park(暫停)if?(shouldParkAfterFailedAcquire(p,?node)?&&parkAndCheckInterrupt())interrupted?=?true;}}?finally?{if?(failed)cancelAcquire(node);}}acquireQueued()方法主要執(zhí)行以下幾件事:
死循環(huán)處理等待線程中的前置節(jié)點,并嘗試獲取鎖,若是p == head && tryAcquire(arg),則跳出循環(huán),即獲取鎖成功。
若是獲取鎖不成功shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()就會將線程暫停。
在acquire(int arg)方法中,最后若是條件成立,執(zhí)行下面的源碼:
selfInterrupt();//?實際執(zhí)行的代碼為 Thread.currentThread().interrupt();即嘗試獲取鎖失敗,就會將鎖加入等待的線程隊列中,并讓線程處于中斷等待。公平鎖lock()方法執(zhí)行的原理圖如下:
之所以畫這些原理的的原因,是為后面寫一個自己的鎖做鋪墊,因為你要實現(xiàn)和前人差不多的東西,你必須了解該東西執(zhí)行的步驟,最后得出的結(jié)果,執(zhí)行的過程是怎么樣的。
有了流程圖,在后面的實現(xiàn)自己的東西才能一步一步的進行。這也是閱讀源碼的必要之一。
在lock()方法,其實在lock()方法中,已經(jīng)包含了兩方面:
鎖方法lock()。
嘗試獲取鎖方法tryAquire()。
接下來,我們來看一下unlock()方法的源碼。
??public?void?unlock()?{sync.release(1);}直接調(diào)用release(1)方法,來看release方法源碼如下:
????public?final?boolean?release(int?arg)?{//?嘗試釋放當前節(jié)點if?(tryRelease(arg))?{//?取出頭節(jié)點Node?h?=?head;if?(h?!=?null?&&?h.waitStatus?!=?0)//?釋放鎖后要即使喚醒等待的線程來獲取鎖unparkSuccessor(h);return?true;}return?false;}通過調(diào)用tryRelease(arg),嘗試釋放當前節(jié)點,若是釋放鎖成功,就會獲取的等待隊列中的頭節(jié)點,就會即使喚醒等待隊列中的等待線程來獲取鎖。接下來看看tryRelease(arg)的源碼如下:
//?嘗試釋放鎖protected?final?boolean?tryRelease(int?releases)?{//?將當前狀態(tài)值-1int?c?=?getState()?-?releases;//?判斷當前線程是否是鎖的擁有者,若不是直接拋出異常,非法操作,直接一點的解釋就是,你都沒有擁有鎖,還來釋放鎖,這不是騙人的嘛if?(Thread.currentThread()?!=?getExclusiveOwnerThread())throw?new?IllegalMonitorStateException();boolean?free?=?false;//執(zhí)行釋放鎖操作?1.若狀態(tài)值=0???2.將當前的鎖的擁有者設(shè)為nullif?(c?==?0)?{free?=?true;setExclusiveOwnerThread(null);}//?重新更新status的狀態(tài)值setState(c);return?free;}總結(jié)上面的幾個方法,unlock釋放鎖方法的執(zhí)行原理圖如下:
對于非公平鎖與公平鎖的區(qū)別,在非公平鎖嘗試獲取鎖中不會執(zhí)行hasQueuedPredecessors()去判斷是否隊列中還有等待的前置節(jié)點線程。
如下面的非公平鎖,嘗試獲取鎖nonfairTryAcquire()源碼如下:
final?boolean?nonfairTryAcquire(int?acquires)?{final?Thread?current?=?Thread.currentThread();int?c?=?getState();if?(c?==?0)?{//?直接就將status-1,并不會判斷是否還有前置線程在等待if?(compareAndSetState(0,?acquires))?{setExclusiveOwnerThread(current);return?true;}}else?if?(current?==?getExclusiveOwnerThread())?{int?nextc?=?c?+?acquires;if?(nextc?<?0)?//?overflowthrow?new?Error("Maximum?lock?count?exceeded");setState(nextc);return?true;}return?false;}以上就是公平鎖和非公平鎖的主要的核心方法的源碼,接下來我們實現(xiàn)自己的一個鎖,首先依據(jù)前面的分析中,要實現(xiàn)自己的鎖,擁有的鎖的核心屬性如下:
狀態(tài)值status,0為未占用鎖,1未占用鎖,并且是線程安全的。
等待線程隊列,用于存放獲取鎖的等待線程。
當前線程的擁有者。
lock鎖的核心的Api如下:
lock方法
trylock方法
unlock方法
依據(jù)以上的核心思想來實現(xiàn)自己的鎖,首先定義狀態(tài)值status,使用的是AtomicInteger原子變量來存放狀態(tài)值,實現(xiàn)該狀態(tài)值的并發(fā)安全和可見性。定義如下:
//?線程的狀態(tài)?0表示當前沒有線程占用???1表示有線程占用 AtomicInteger?status?=new?AtomicInteger();接下來定義等待線程隊列,使用LinkedBlockingQueue隊列來裝線程,定義如下:
//?等待的線程 LinkedBlockingQueue<Thread>?waiters?=?new?LinkedBlockingQueue<Thread>();最后的屬性為當前鎖的擁有者,直接就用Thread來封裝,定義如下:
//?當前線程擁有者 Thread?ownerThread?=null;接下來定義lock()方法,依據(jù)上面的源碼分析,在lock方法中主要執(zhí)行的幾件事如下:
死循環(huán)的處理等待線程隊列中的線程,知道獲取鎖成功,將該線程從隊列中刪除,跳出循環(huán)。
獲取鎖不成功,線程處于暫停等待。
然后是trylock方法,依據(jù)上面的源碼分析,在trylock中主要執(zhí)行的以下幾件事:
判斷當前擁有鎖的線程的狀態(tài)是否為0,為0,執(zhí)行狀態(tài)值+1,并將當前線程設(shè)置為鎖擁有者。
實現(xiàn)鎖可重入
最后就是unlock方法,依據(jù)上面的源碼分析,在unlock中主要執(zhí)行的事情如下:
判斷當前線程是否是鎖擁有者,若不是直接拋出異常。
判斷狀態(tài)值是否為0,并將鎖擁有者清空,喚醒等待的線程。
以上就是實現(xiàn)自己的非公平的可重入鎖,lock的源碼其實并不復雜,只要認真看都能看懂,在閱讀源碼的過程中,會遇到比較復雜的問題。遇到問題不要慌,網(wǎng)上查詢資料,相信很多都能找到答案,因為java的生態(tài)如此完善,幾乎90%的東西網(wǎng)上都會有,只要沉得住氣,相信一定會有所收獲。
總結(jié)
以上是生活随笔為你收集整理的深入Lock锁底层原理实现,手写一个可重入锁的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 编程语言圣经(卷一)
- 下一篇: 文件句柄?文件描述符?傻傻分不清楚