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