java同步之如何寫一個(gè)鎖Lock
問題
(1)自己動(dòng)手寫一個(gè)鎖需要哪些知識(shí)?
(2)自己動(dòng)手寫一個(gè)鎖到底有多簡單?
(3)自己能不能寫出來一個(gè)完美的鎖?
簡介
本篇文章的目標(biāo)一是自己動(dòng)手寫一個(gè)鎖,這個(gè)鎖的功能很簡單,能進(jìn)行正常的加鎖、解鎖操作。
本篇文章的目標(biāo)二是通過自己動(dòng)手寫一個(gè)鎖,能更好地理解后面章節(jié)將要學(xué)習(xí)的AQS及各種同步器實(shí)現(xiàn)的原理。
分析
自己動(dòng)手寫一個(gè)鎖需要準(zhǔn)備些什么呢?
首先,在上一章學(xué)習(xí)synchronized的時(shí)候我們說過它的實(shí)現(xiàn)原理是更改對(duì)象頭中的MarkWord,標(biāo)記為已加鎖或未加鎖。
但是,我們自己是無法修改對(duì)象頭信息的,那么我們可不可以用一個(gè)變量來代替呢?
比如,這個(gè)變量的值為1的時(shí)候就說明已加鎖,變量值為0的時(shí)候就說明未加鎖,我覺得可行。
其次,我們要保證多個(gè)線程對(duì)上面我們定義的變量的爭用是可控的,所謂可控即同時(shí)只能有一個(gè)線程把它的值修改為1,且當(dāng)它的值為1的時(shí)候其它線程不能再修改它的值,這種是不是就是典型的CAS操作,所以我們需要使用Unsafe這個(gè)類來做CAS操作。
然后,我們知道在多線程的環(huán)境下,多個(gè)線程對(duì)同一個(gè)鎖的爭用肯定只有一個(gè)能成功,那么,其它的線程就要排隊(duì),所以我們還需要一個(gè)隊(duì)列。
最后,這些線程排隊(duì)的時(shí)候干嘛呢?它們不能再繼續(xù)執(zhí)行自己的程序,那就只能阻塞了,阻塞完了當(dāng)輪到這個(gè)線程的時(shí)候還要喚醒,所以我們還需要Unsfae這個(gè)類來阻塞(park)和喚醒(unpark)線程。
基于以上四點(diǎn),我們需要的神器大致有:一個(gè)變量、一個(gè)隊(duì)列、執(zhí)行CAS/park/unpark的Unsafe類。
大概的流程圖如下圖所示:
關(guān)于Unsafe類的相關(guān)講解請(qǐng)參考之前發(fā)的文章:
解決
一個(gè)變量
這個(gè)變量只支持同時(shí)只有一個(gè)線程能把它修改為1,所以它修改完了一定要讓其它線程可見,因此,這個(gè)變量需要使用volatile來修飾。
private volatile int state;
CAS
這個(gè)變量的修改必須是原子操作,所以我們需要CAS更新它,我們這里使用Unsafe來直接CAS更新int類型的state。
當(dāng)然,這個(gè)變量如果直接使用AtomicInteger也是可以的,不過,既然我們學(xué)習(xí)了更底層的Unsafe類那就應(yīng)該用(浪)起來。
private boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
一個(gè)隊(duì)列
隊(duì)列的實(shí)現(xiàn)有很多,數(shù)組、鏈表都可以,我們這里采用鏈表,畢竟鏈表實(shí)現(xiàn)隊(duì)列相對(duì)簡單一些,不用考慮擴(kuò)容等問題。
這個(gè)隊(duì)列的操作很有特點(diǎn):
放元素的時(shí)候都是放到尾部,且可能是多個(gè)線程一起放,所以對(duì)尾部的操作要CAS更新;
喚醒一個(gè)元素的時(shí)候從頭部開始,但同時(shí)只有一個(gè)線程在操作,即獲得了鎖的那個(gè)線程,所以對(duì)頭部的操作不需要CAS去更新。
private static class Node { // 存儲(chǔ)的元素為線程 Thread thread; // 前一個(gè)節(jié)點(diǎn)(可以沒有,但實(shí)現(xiàn)起來很困難) Node prev; // 后一個(gè)節(jié)點(diǎn) Node next; public Node() { } public Node(Thread thread, Node prev) { this.thread = thread; this.prev = prev; } } // 鏈表頭 private volatile Node head; // 鏈表尾 private volatile Node tail; // 原子更新tail字段 private boolean compareAndSetTail(Node expect, Node update) { return unsafe.compareAndSwapObject(this, tailOffset, expect, update); }
這個(gè)隊(duì)列很簡單,存儲(chǔ)的元素是線程,需要有指向下一個(gè)待喚醒的節(jié)點(diǎn),前一個(gè)節(jié)點(diǎn)可有可無,但是沒有實(shí)現(xiàn)起來很困難,不信學(xué)完這篇文章你試試。
加鎖
public void lock() { // 嘗試更新state字段,更新成功說明占有了鎖 if (compareAndSetState(0, 1)) { return; } // 未更新成功則入隊(duì) Node node = enqueue(); Node prev = node.prev; // 再次嘗試獲取鎖,需要檢測上一個(gè)節(jié)點(diǎn)是不是head,按入隊(duì)順序加鎖 while (node.prev != head || !compareAndSetState(0, 1)) { // 未獲取到鎖,阻塞 unsafe.park(false, 0L); } // 下面不需要原子更新,因?yàn)橥瑫r(shí)只有一個(gè)線程訪問到這里 // 獲取到鎖了且上一個(gè)節(jié)點(diǎn)是head // head后移一位 head = node; // 清空當(dāng)前節(jié)點(diǎn)的內(nèi)容,協(xié)助GC node.thread = null; // 將上一個(gè)節(jié)點(diǎn)從鏈表中剔除,協(xié)助GC node.prev = null; prev.next = null; } // 入隊(duì) private Node enqueue() { while (true) { // 獲取尾節(jié)點(diǎn) Node t = tail; // 構(gòu)造新節(jié)點(diǎn) Node node = new Node(Thread.currentThread(), t); // 不斷嘗試原子更新尾節(jié)點(diǎn) if (compareAndSetTail(t, node)) { // 更新尾節(jié)點(diǎn)成功了,讓原尾節(jié)點(diǎn)的next指針指向當(dāng)前節(jié)點(diǎn) t.next = node; return node; } } }
(1)嘗試獲取鎖,成功了就直接返回;
(2)未獲取到鎖,就進(jìn)入隊(duì)列排隊(duì);
(3)入隊(duì)之后,再次嘗試獲取鎖;
(4)如果不成功,就阻塞;
(5)如果成功了,就把頭節(jié)點(diǎn)后移一位,并清空當(dāng)前節(jié)點(diǎn)的內(nèi)容,且與上一個(gè)節(jié)點(diǎn)斷絕關(guān)系;
(6)加鎖結(jié)束;
解鎖
// 解鎖 public void unlock() { // 把state更新成0,這里不需要原子更新,因?yàn)橥瑫r(shí)只有一個(gè)線程訪問到這里 state = 0; // 下一個(gè)待喚醒的節(jié)點(diǎn) Node next = head.next; // 下一個(gè)節(jié)點(diǎn)不為空,就喚醒它 if (next != null) { unsafe.unpark(next.thread); } }
(1)把state改成0,這里不需要CAS更新,因?yàn)楝F(xiàn)在還在加鎖中,只有一個(gè)線程去更新,在這句之后就釋放了鎖;
(2)如果有下一個(gè)節(jié)點(diǎn)就喚醒它;
(3)喚醒之后就會(huì)接著走上面lock()方法的while循環(huán)再去嘗試獲取鎖;
(4)喚醒的線程不是百分之百能獲取到鎖的,因?yàn)檫@里state更新成0的時(shí)候就解鎖了,之后可能就有線程去嘗試加鎖了。
測試
上面完整的鎖的實(shí)現(xiàn)就完了,是不是很簡單,但是它是不是真的可靠呢,敢不敢來試試?!
直接上測試代碼:
private static int count = 0; public static void main(String[] args) throws InterruptedException { MyLock lock = new MyLock(); CountDownLatch countDownLatch = new CountDownLatch(1000); IntStream.range(0, 1000).forEach(i -> new Thread(() -> { lock.lock(); try { IntStream.range(0, 10000).forEach(j -> { count++; }); } finally { lock.unlock(); } // System.out.println(Thread.currentThread().getName()); countDownLatch.countDown(); }, "tt-" + i).start()); countDownLatch.await(); System.out.println(count); }
運(yùn)行這段代碼的結(jié)果是總是打印出10000000(一千萬),說明我們的鎖是正確的、可靠的、完美的。
總結(jié)
(1)自己動(dòng)手寫一個(gè)鎖需要做準(zhǔn)備:一個(gè)變量、一個(gè)隊(duì)列、Unsafe類。
(2)原子更新變量為1說明獲得鎖成功;
(3)原子更新變量為1失敗說明獲得鎖失敗,進(jìn)入隊(duì)列排隊(duì);
(4)更新隊(duì)列尾節(jié)點(diǎn)的時(shí)候是多線程競爭的,所以要使用原子更新;
(5)更新隊(duì)列頭節(jié)點(diǎn)的時(shí)候只有一個(gè)線程,不存在競爭,所以不需要使用原子更新;
(6)隊(duì)列節(jié)點(diǎn)中的前一個(gè)節(jié)點(diǎn)prev的使用很巧妙,沒有它將很難實(shí)現(xiàn)一個(gè)鎖,只有寫過的人才明白,不信你試試^^
彩蛋
(1)我們實(shí)現(xiàn)的鎖支持可重入嗎?
答:不可重入,因?yàn)槲覀兠看沃话裺tate更新為1。如果要支持可重入也很簡單,獲取鎖時(shí)檢測鎖是不是被當(dāng)前線程占有著,如果是就把state的值加1,釋放鎖時(shí)每次減1即可,減為0時(shí)表示鎖已釋放。
(2)我們實(shí)現(xiàn)的鎖是公平鎖還是非公平鎖?
答:非公平鎖,因?yàn)楂@取鎖的時(shí)候我們先嘗試了一次,這里并不是嚴(yán)格的排隊(duì),所以是非公平鎖。
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
詳解如何在Java中使用阿里云對(duì)象存儲(chǔ)OSS
Java是世界上最流行的編程語言之一,擁有著廣泛的應(yīng)用場景和強(qiáng)大的生態(tài)系統(tǒng),阿里云對(duì)象存儲(chǔ) OSS 是一種企業(yè)級(jí)的云存儲(chǔ)服務(wù),本文將介紹如何在 Java 中使用阿里云對(duì)象存儲(chǔ) OSS,并寫一點(diǎn)相應(yīng)的代碼示例供大家參考2023-06-06SpringSecurity在SpringBoot中的自動(dòng)裝配過程
這篇文章主要介紹了SpringSecurity在SpringBoot中的自動(dòng)裝配過程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07java微信公眾號(hào)開發(fā)(搭建本地測試環(huán)境)
這篇文章主要介紹了java微信公眾號(hào)開發(fā),主要內(nèi)容有測試公眾號(hào)與本地測試環(huán)境搭建,需要的朋友可以參考下2015-12-12MyBatis控制臺(tái)顯示SQL語句的方法實(shí)現(xiàn)
這篇文章主要介紹了MyBatis控制臺(tái)顯示SQL語句的方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03Spring Boot 驗(yàn)證碼框架 CAPTCHA詳解
這篇文章主要介紹了Spring Boot 驗(yàn)證碼框架 CAPTCHA詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03