最詳細(xì)的圖文解析Java各種鎖(終極篇)

前言

線程并發(fā)系列文章:

Java 線程基礎(chǔ)
Java 線程狀態(tài)
Java “優(yōu)雅”地中斷線程-實(shí)踐篇
Java “優(yōu)雅”地中斷線程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有誤
Java Unsafe/CAS/LockSupport 應(yīng)用與原理
Java 并發(fā)"鎖"的本質(zhì)(一步步實(shí)現(xiàn)鎖)
Java Synchronized實(shí)現(xiàn)互斥之應(yīng)用與源碼初探
Java 對象頭分析與使用(Synchronized相關(guān))
Java Synchronized 偏向鎖/輕量級(jí)鎖/重量級(jí)鎖的演變過程
Java Synchronized 重量級(jí)鎖原理深入剖析上(互斥篇)
Java Synchronized 重量級(jí)鎖原理深入剖析下(同步篇)
Java并發(fā)之 AQS 深入解析(上)
Java并發(fā)之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 詳解
Java 并發(fā)之 ReentrantLock 深入分析(與Synchronized區(qū)別)
Java 并發(fā)之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(應(yīng)用篇)
最詳細(xì)的圖文解析Java各種鎖(終極篇)
線程池必懂系列

前面的十幾篇文章都是從源碼的角度分析線程并發(fā)涉及到的知識(shí)點(diǎn)著隆,本篇將重點(diǎn)總結(jié)奖磁、歸納耸彪、提煉知識(shí)點(diǎn)陈瘦,盡量少貼代碼。遇到有疑惑的點(diǎn)躯舔,請查看對應(yīng)文章的分析。
通過本篇文章,你將了解到:

1髓迎、鎖的全家福
2、如何驗(yàn)證公平/非公平鎖
3建丧、底層如何獲取鎖/釋放鎖
4排龄、自旋鎖與自適應(yīng)自旋
5、為什么需要等待/通知機(jī)制

1翎朱、鎖的全家福

image.png

2橄维、如何驗(yàn)證公平/非公平鎖

公平與非公平區(qū)別之處在于獲取鎖時(shí)的策略。


image.png

如上圖:

1拴曲、線程1持有鎖争舞。
2、線程2澈灼、線程3竞川、線程4 在同步隊(duì)列里排隊(duì)等候鎖。

這時(shí)線程5也想要獲取鎖叁熔,根據(jù)公平與否分為兩種不同策略委乌。

公平鎖

線程5先判斷同步隊(duì)列是是否有線程在等待,明顯地此時(shí)同步隊(duì)列里有線程在等待荣回,于是線程5加入到同步隊(duì)列的尾部等待遭贸。

非公平鎖

1、線程5不管同步隊(duì)列是否有線程等待心软,管他三七二十一先去搶鎖再說革砸。若是運(yùn)氣好就能直接撿到便宜獲取了鎖,若是失敗再去排隊(duì)糯累。
2算利、線程5還是有機(jī)會(huì)撿便宜的,若是此時(shí)線程1剛好釋放了鎖泳姐,并喚醒線程2效拭,線程2醒過來后去獲取鎖。若在線程2獲取鎖之前線程5就去搶鎖了,那么它會(huì)成功缎患。它的成功對于線程2慕的、線程3、線程4來說是不公平的挤渔。

我們知道ReentrantLock 可實(shí)現(xiàn)公平/非公平鎖肮街,來驗(yàn)證一下。

先來驗(yàn)證公平鎖:

public class TestThread {
    private ReentrantLock reentrantLock = new ReentrantLock(true);
    private void testLock() {
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(runnable);
            thread.setName("線程" + (i + 1));
            thread.start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " 啟動(dòng)了判导,準(zhǔn)備獲取鎖");
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + " 獲取了鎖");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    };

    public static void main(String args[]) {
        TestThread testThread = new TestThread();
        testThread.testLock();
    }
}

打印如下:


image.png

可以看出嫉父,線程2、3眼刃、4绕辖、5 按順序獲取鎖,實(shí)際上拿到鎖也是按照這順序的擂红。
因此仪际,符合先到先得,是公平的昵骤。

再來驗(yàn)證非公平鎖

public class TestThread {
    private ReentrantLock reentrantLock = new ReentrantLock(false);
    private void testLock() {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(runnable);
            thread.setName("線程" + (i + 1));
            thread.start();
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void testUnfair() {
        try {
            Thread.sleep(500);
            while (true) {
                System.out.println("+++++++我搶...+++++++");
                boolean isLock = reentrantLock.tryLock();
                if (isLock) {
                    System.out.println("========我搶到鎖了!!!===========");
                    reentrantLock.unlock();
                    return;
                }
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " 啟動(dòng)了树碱,準(zhǔn)備獲取鎖");
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + " 獲取了鎖");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    };

    public static void main(String args[]) {
        TestThread testThread = new TestThread();
        testThread.testLock();
        testThread.testUnfair();
    }
}

打印如下:


image.png
image.png

這倆張圖結(jié)合來看:

1、第一張圖:線程1~線程10 依次調(diào)用lock搶鎖变秦,然后主線程開始搶鎖成榜。
2、只要有一次能夠證明主線成比線程1~線程10之間的某個(gè)線程先獲得鎖伴栓,那么就證明該鎖為非公平鎖伦连。
3雨饺、第二張圖:主線程比線程4~線程10 先獲得了鎖钳垮,說明過程是非公平的。

值得注意的是:

此處使用tryLock()搶占鎖额港,tryLock()和lock(非公平模式)核心邏輯是一樣的饺窿。

3、底層如何獲取鎖/釋放鎖

一直在提線程獲取了鎖移斩,線程釋放了鎖肚医,到底這個(gè)邏輯如何實(shí)現(xiàn)的呢?
從第一張全家福的圖向瓷,可以看出鎖的基本數(shù)據(jù)結(jié)構(gòu)包含:

共享鎖變量肠套、volatile、CAS猖任、同步隊(duì)列你稚。

假設(shè)設(shè)定共享變量為:volatile int threadId。

threadId == 0表示當(dāng)前沒有線程獲取鎖,thread !=0 表示有線程占有了鎖刁赖。

獲取鎖

1搁痛、線程調(diào)用 CAS(threadId, 0, 1),預(yù)期threadId == 0, 若是符合預(yù)期宇弛,則將threadId設(shè)置為1鸡典,CAS成功說明成功獲取了鎖。
2枪芒、若是CAS失敗彻况,說明threadId != 0,進(jìn)而說明有已經(jīng)有別的線程修改了threadId病苗,因此線程獲取鎖失敗疗垛,然后加入到同步隊(duì)列。

釋放鎖

1硫朦、持有鎖的線程不需要鎖后要釋放鎖贷腕,假設(shè)是獨(dú)占鎖(互斥),因?yàn)橥瑫r(shí)只有一個(gè)線程能獲取鎖咬展,因此釋放鎖時(shí)修改threadId不需要CAS泽裳,直接threadId == 0,說明釋放鎖成功破婆。
2涮总、成功后,喚醒在同步隊(duì)列里等待的線程祷舀。

synchronized 和 AQS 獲取/釋放鎖核心思想就是上面幾步瀑梗,只是控制得更復(fù)雜,精細(xì)裳扯,考慮得更全面抛丽。

注:CAS(threadId, xx, xx)是偽代碼

4、自旋鎖與自適應(yīng)自旋

很多文章說CAS是自旋鎖饰豺,這說法是有問題的亿鲜,本質(zhì)上沒有完全理解CAS功能和鎖。

1冤吨、CAS 全稱是比較與交換蒿柳,若是內(nèi)存值與期望值一致,說明沒有其它線程更改目標(biāo)變量漩蟆,因此可以放心地將目標(biāo)變量修改為新值垒探。
2、CAS 是原子操作怠李,底層是CPU指令圾叼。
3仔引、CAS 只是一次嘗試修改目標(biāo)變量的操作,結(jié)果要么成功褐奥,要么失敗咖耘,最后調(diào)用都會(huì)返回。

通過上個(gè)小結(jié)的分析撬码,我們知道synchronized儿倒、AQS底層獲取/釋放鎖都是依賴CAS的,難道說synchronized呜笑、AQS 也是自旋鎖夫否,顯然不是。

自旋鎖是不會(huì)阻塞的叫胁,而CAS也不會(huì)阻塞凰慈,因此可以利用CAS實(shí)現(xiàn)自旋鎖:

    class MyLock {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        private void lock() {
            boolean suc = false;
            do {
                //底層是CAS
                suc = atomicInteger.compareAndSet(0, 1);
            } while (!suc);
        }   
    }

如上所示,自定義鎖MyLock驼鹅,線程1微谓,線程2分別調(diào)用lock()上鎖。

1输钩、線程1調(diào)用lock()豺型,因?yàn)閍tomicInteger== 0,所以suc == true买乃,線程1成功獲取鎖姻氨。
2、此時(shí)線程2也調(diào)用lock()剪验,因?yàn)閍tomicInteger==1肴焊,說明鎖被占用了,所以suc==false功戚,然而線程2并不阻塞娶眷,一直循環(huán)去修改。只要線程1不釋放鎖疫铜,那么線程2永遠(yuǎn)獲取不了鎖茂浮。

以上就是自旋鎖的實(shí)現(xiàn)双谆,可以看出:

1壳咕、自旋鎖最大限度避免了線程掛起/與喚醒,避免上下文切換顽馋,但是無限制的自旋也會(huì)徒勞占用CPU資源谓厘。
2、因此自選鎖適用于線程執(zhí)行臨界區(qū)比較快的場景寸谜,也就是獲得鎖后竟稳,快速釋放了鎖。

既想要自旋,又要避免無限制自旋他爸,因此引入了自適應(yīng)自旋:

    class MyLock {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        //最大自旋次數(shù)
        final int MAX_COUNT = 10;
        int count = 0;
        private void lock() {
            boolean suc = false;
            while (!suc && count <= MAX_COUNT) {
                //底層是CAS
                suc = atomicInteger.compareAndSet(0, 1);
                if (!suc)
                    Thread.yield();
                count++;
            }
        }
    }

可以看出聂宾,給自旋設(shè)置了最大自旋次數(shù),若還是沒能獲取到鎖诊笤,則退出死循環(huán)系谐。

實(shí)際上synchronized、ReentrantReadWriteLock 等的實(shí)現(xiàn)里讨跟,同樣為了盡量避免線程掛起/喚醒纪他,在搶占鎖的過程中也是采用了自旋(自適應(yīng)自旋)的思想,但這只是它們鎖實(shí)現(xiàn)的以小部分晾匠,它們并不是自旋鎖茶袒。

5、為什么需要等待/通知機(jī)制

先看獨(dú)占鎖的偽代碼:

    //Thread1
    myLock.lock();
    {
        //臨界區(qū)代碼
    }
    myLock.unLock();

    //Thread2
    myLock.lock();
    {
        //臨界區(qū)代碼
    }
    myLock.unLock();

Thread1凉馆、Thread2 互斥拿到鎖后各干各的薪寓,互不干涉,相安無事澜共。
若是現(xiàn)在Thread1预愤、Thread2 需要配合做事,如:

    //Thread1
    myLock.lock();
    {
        //臨界區(qū)代碼
        while (flag == false)
            wait();
        //繼續(xù)做事
    }
    myLock.unLock();

    //Thread2
    myLock.lock();
    {
        //臨界區(qū)代碼
        flag = true;
        notify();
        //繼續(xù)做事
    }
    myLock.unLock();

如上代碼咳胃,Thread1需要判斷flag == true才會(huì)往下運(yùn)行植康,而這個(gè)值需要Thread2來修改,Thread1展懈、Thread2 兩者間有協(xié)作關(guān)系销睁。于是Thread1需要調(diào)用wait 釋放鎖,并阻塞等待存崖。Thread2在Thread1釋放鎖后拿到鎖冻记,修改flag,然后notify 喚醒Thread1(喚醒時(shí)機(jī)在Thread2執(zhí)行完臨界區(qū)代碼并釋放鎖后)来惧。Thread1 被喚醒后繼續(xù)搶鎖冗栗,然后判斷flag==true,繼續(xù)做事供搀。
于是隅居,Thread1、Thread2愉快配合完成工作葛虐。
為啥wait/notify 需要先獲取鎖呢胎源?flag 是線程間共享變量,需要在并發(fā)條件下正確訪問屿脐,因此需要鎖涕蚤。

至此宪卿,線程并發(fā)系列文章暫時(shí)告一段落了。大家對這系列文章有疑惑万栅,請?jiān)u論留言佑钾。

本文基于jdk 1.8。

您若喜歡烦粒,請點(diǎn)贊次绘、關(guān)注,您的鼓勵(lì)是我前進(jìn)的動(dòng)力

持續(xù)更新中撒遣,和我一起步步為營系統(tǒng)邮偎、深入學(xué)習(xí)Android/Java

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市义黎,隨后出現(xiàn)的幾起案子禾进,更是在濱河造成了極大的恐慌,老刑警劉巖廉涕,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泻云,死亡現(xiàn)場離奇詭異,居然都是意外死亡狐蜕,警方通過查閱死者的電腦和手機(jī)宠纯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來层释,“玉大人婆瓜,你說我怎么就攤上這事」备幔” “怎么了廉白?”我有些...
    開封第一講書人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長乖寒。 經(jīng)常有香客問我猴蹂,道長,這世上最難降的妖魔是什么楣嘁? 我笑而不...
    開封第一講書人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任磅轻,我火速辦了婚禮,結(jié)果婚禮上逐虚,老公的妹妹穿的比我還像新娘聋溜。我一直安慰自己,他們只是感情好痊班,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開白布勤婚。 她就那樣靜靜地躺著摹量,像睡著了一般涤伐。 火紅的嫁衣襯著肌膚如雪馒胆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,255評(píng)論 1 308
  • 那天凝果,我揣著相機(jī)與錄音祝迂,去河邊找鬼。 笑死器净,一個(gè)胖子當(dāng)著我的面吹牛型雳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播山害,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼纠俭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了浪慌?” 一聲冷哼從身側(cè)響起冤荆,我...
    開封第一講書人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎权纤,沒想到半個(gè)月后钓简,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汹想,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年外邓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片古掏。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡损话,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出槽唾,到底是詐尸還是另有隱情席镀,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布夏漱,位于F島的核電站豪诲,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏挂绰。R本人自食惡果不足惜屎篱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望葵蒂。 院中可真熱鬧交播,春花似錦、人聲如沸践付。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽永高。三九已至隧土,卻和暖如春提针,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背曹傀。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來泰國打工辐脖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人皆愉。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓嗜价,卻偏偏與公主長得像,于是被迫代替她去往敵國和親幕庐。 傳聞我的和親對象是個(gè)殘疾皇子久锥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容