java 同步锁_死磕 java同步系列之自己动手写一个锁Lock
問題
(1)自己動手寫一個鎖需要哪些知識?
(2)自己動手寫一個鎖到底有多簡單?
(3)自己能不能寫出來一個完美的鎖?
簡介
本篇文章的目標(biāo)一是自己動手寫一個鎖,這個鎖的功能很簡單,能進(jìn)行正常的加鎖、解鎖操作。
本篇文章的目標(biāo)二是通過自己動手寫一個鎖,能更好地理解后面章節(jié)將要學(xué)習(xí)的AQS及各種同步器實現(xiàn)的原理。
分析
自己動手寫一個鎖需要準(zhǔn)備些什么呢?
首先,在上一章學(xué)習(xí)synchronized的時候我們說過它的實現(xiàn)原理是更改對象頭中的MarkWord,標(biāo)記為已加鎖或未加鎖。
但是,我們自己是無法修改對象頭信息的,那么我們可不可以用一個變量來代替呢?
比如,這個變量的值為1的時候就說明已加鎖,變量值為0的時候就說明未加鎖,我覺得可行。
其次,我們要保證多個線程對上面我們定義的變量的爭用是可控的,所謂可控即同時只能有一個線程把它的值修改為1,且當(dāng)它的值為1的時候其它線程不能再修改它的值,這種是不是就是典型的CAS操作,所以我們需要使用Unsafe這個類來做CAS操作。
然后,我們知道在多線程的環(huán)境下,多個線程對同一個鎖的爭用肯定只有一個能成功,那么,其它的線程就要排隊,所以我們還需要一個隊列。
最后,這些線程排隊的時候干嘛呢?它們不能再繼續(xù)執(zhí)行自己的程序,那就只能阻塞了,阻塞完了當(dāng)輪到這個線程的時候還要喚醒,所以我們還需要Unsfae這個類來阻塞(park)和喚醒(unpark)線程。
基于以上四點,我們需要的神器大致有:一個變量、一個隊列、執(zhí)行CAS/park/unpark的Unsafe類。
大概的流程圖如下圖所示:
關(guān)于Unsafe類的相關(guān)講解請參考彤哥之前發(fā)的文章:
【死磕 java魔法類之Unsafe解析】
解決
一個變量
這個變量只支持同時只有一個線程能把它修改為1,所以它修改完了一定要讓其它線程可見,因此,這個變量需要使用volatile來修飾。
privateCAS
這個變量的修改必須是原子操作,所以我們需要CAS更新它,我們這里使用Unsafe來直接CAS更新int類型的state。
當(dāng)然,這個變量如果直接使用AtomicInteger也是可以的,不過,既然我們學(xué)習(xí)了更底層的Unsafe類那就應(yīng)該用(浪)起來。
private一個隊列
隊列的實現(xiàn)有很多,數(shù)組、鏈表都可以,我們這里采用鏈表,畢竟鏈表實現(xiàn)隊列相對簡單一些,不用考慮擴(kuò)容等問題。
這個隊列的操作很有特點:
放元素的時候都是放到尾部,且可能是多個線程一起放,所以對尾部的操作要CAS更新;
喚醒一個元素的時候從頭部開始,但同時只有一個線程在操作,即獲得了鎖的那個線程,所以對頭部的操作不需要CAS去更新。
private這個隊列很簡單,存儲的元素是線程,需要有指向下一個待喚醒的節(jié)點,前一個節(jié)點可有可無,但是沒有實現(xiàn)起來很困難,不信學(xué)完這篇文章你試試。
加鎖
public(1)嘗試獲取鎖,成功了就直接返回;
(2)未獲取到鎖,就進(jìn)入隊列排隊;
(3)入隊之后,再次嘗試獲取鎖;
(4)如果不成功,就阻塞;
(5)如果成功了,就把頭節(jié)點后移一位,并清空當(dāng)前節(jié)點的內(nèi)容,且與上一個節(jié)點斷絕關(guān)系;
(6)加鎖結(jié)束;
解鎖
// 解鎖(1)把state改成0,這里不需要CAS更新,因為現(xiàn)在還在加鎖中,只有一個線程去更新,在這句之后就釋放了鎖;
(2)如果有下一個節(jié)點就喚醒它;
(3)喚醒之后就會接著走上面lock()方法的while循環(huán)再去嘗試獲取鎖;
(4)喚醒的線程不是百分之百能獲取到鎖的,因為這里state更新成0的時候就解鎖了,之后可能就有線程去嘗試加鎖了。
測試
上面完整的鎖的實現(xiàn)就完了,是不是很簡單,但是它是不是真的可靠呢,敢不敢來試試?!
直接上測試代碼:
private運行這段代碼的結(jié)果是總是打印出10000000(一千萬),說明我們的鎖是正確的、可靠的、完美的。
總結(jié)
(1)自己動手寫一個鎖需要做準(zhǔn)備:一個變量、一個隊列、Unsafe類。
(2)原子更新變量為1說明獲得鎖成功;
(3)原子更新變量為1失敗說明獲得鎖失敗,進(jìn)入隊列排隊;
(4)更新隊列尾節(jié)點的時候是多線程競爭的,所以要使用原子更新;
(5)更新隊列頭節(jié)點的時候只有一個線程,不存在競爭,所以不需要使用原子更新;
(6)隊列節(jié)點中的前一個節(jié)點prev的使用很巧妙,沒有它將很難實現(xiàn)一個鎖,只有寫過的人才明白,不信你試試^^
彩蛋
(1)我們實現(xiàn)的鎖支持可重入嗎?
答:不可重入,因為我們每次只把state更新為1。如果要支持可重入也很簡單,獲取鎖時檢測當(dāng)前鎖是不是當(dāng)前線程占有著,如果是就把state的值加1,釋放鎖時每次減1即可,減為0時表示鎖已釋放。
(2)我們實現(xiàn)的鎖是公平鎖還是非公平鎖?
答:非公平鎖,因為獲取鎖的時候我們先嘗試了一次,這里并不是嚴(yán)格的排隊,所以是非公平鎖。
注:下一章我們將開始分析傳說中的AQS,這章是基礎(chǔ),請各位老鐵務(wù)必搞明白。
推薦閱讀
歡迎關(guān)注我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。
總結(jié)
以上是生活随笔為你收集整理的java 同步锁_死磕 java同步系列之自己动手写一个锁Lock的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2019计算机开机号003期,2019年
- 下一篇: 消消乐实现下坠_JavaScript有多