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

前言

線程并發系列文章:

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

前面的十幾篇文章都是從源碼的角度分析線程并發涉及到的知識點,本篇將重點總結、歸納、提煉知識點,盡量少貼代碼。遇到有疑惑的點,請查看對應文章的分析。
通過本篇文章,你將了解到:

1、鎖的全家福
2、如何驗證公平/非公平鎖
3、底層如何獲取鎖/釋放鎖
4、自旋鎖與自適應自旋
5、為什么需要等待/通知機制

1、鎖的全家福

image.png

2、如何驗證公平/非公平鎖

公平與非公平區別之處在于獲取鎖時的策略。


image.png

如上圖:

1、線程1持有鎖。
2、線程2、線程3、線程4 在同步隊列里排隊等候鎖。

這時線程5也想要獲取鎖,根據公平與否分為兩種不同策略。

公平鎖

線程5先判斷同步隊列是是否有線程在等待,明顯地此時同步隊列里有線程在等待,于是線程5加入到同步隊列的尾部等待。

非公平鎖

1、線程5不管同步隊列是否有線程等待,管他三七二十一先去搶鎖再說。若是運氣好就能直接撿到便宜獲取了鎖,若是失敗再去排隊。
2、線程5還是有機會撿便宜的,若是此時線程1剛好釋放了鎖,并喚醒線程2,線程2醒過來后去獲取鎖。若在線程2獲取鎖之前線程5就去搶鎖了,那么它會成功。它的成功對于線程2、線程3、線程4來說是不公平的。

我們知道ReentrantLock 可實現公平/非公平鎖,來驗證一下。

先來驗證公平鎖:

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() + " 啟動了,準備獲取鎖");
                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 按順序獲取鎖,實際上拿到鎖也是按照這順序的。
因此,符合先到先得,是公平的。

再來驗證非公平鎖

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() + " 啟動了,準備獲取鎖");
                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

這倆張圖結合來看:

1、第一張圖:線程1~線程10 依次調用lock搶鎖,然后主線程開始搶鎖。
2、只要有一次能夠證明主線成比線程1~線程10之間的某個線程先獲得鎖,那么就證明該鎖為非公平鎖。
3、第二張圖:主線程比線程4~線程10 先獲得了鎖,說明過程是非公平的。

值得注意的是:

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

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

一直在提線程獲取了鎖,線程釋放了鎖,到底這個邏輯如何實現的呢?
從第一張全家福的圖,可以看出鎖的基本數據結構包含:

共享鎖變量、volatile、CAS、同步隊列。

假設設定共享變量為:volatile int threadId。

threadId == 0表示當前沒有線程獲取鎖,thread !=0 表示有線程占有了鎖。

獲取鎖

1、線程調用 CAS(threadId, 0, 1),預期threadId == 0, 若是符合預期,則將threadId設置為1,CAS成功說明成功獲取了鎖。
2、若是CAS失敗,說明threadId != 0,進而說明有已經有別的線程修改了threadId,因此線程獲取鎖失敗,然后加入到同步隊列。

釋放鎖

1、持有鎖的線程不需要鎖后要釋放鎖,假設是獨占鎖(互斥),因為同時只有一個線程能獲取鎖,因此釋放鎖時修改threadId不需要CAS,直接threadId == 0,說明釋放鎖成功。
2、成功后,喚醒在同步隊列里等待的線程。

synchronized 和 AQS 獲取/釋放鎖核心思想就是上面幾步,只是控制得更復雜,精細,考慮得更全面。

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

4、自旋鎖與自適應自旋

很多文章說CAS是自旋鎖,這說法是有問題的,本質上沒有完全理解CAS功能和鎖。

1、CAS 全稱是比較與交換,若是內存值與期望值一致,說明沒有其它線程更改目標變量,因此可以放心地將目標變量修改為新值。
2、CAS 是原子操作,底層是CPU指令。
3、CAS 只是一次嘗試修改目標變量的操作,結果要么成功,要么失敗,最后調用都會返回。

通過上個小結的分析,我們知道synchronized、AQS底層獲取/釋放鎖都是依賴CAS的,難道說synchronized、AQS 也是自旋鎖,顯然不是。

自旋鎖是不會阻塞的,而CAS也不會阻塞,因此可以利用CAS實現自旋鎖:

    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分別調用lock()上鎖。

1、線程1調用lock(),因為atomicInteger== 0,所以suc == true,線程1成功獲取鎖。
2、此時線程2也調用lock(),因為atomicInteger==1,說明鎖被占用了,所以suc==false,然而線程2并不阻塞,一直循環去修改。只要線程1不釋放鎖,那么線程2永遠獲取不了鎖。

以上就是自旋鎖的實現,可以看出:

1、自旋鎖最大限度避免了線程掛起/與喚醒,避免上下文切換,但是無限制的自旋也會徒勞占用CPU資源。
2、因此自選鎖適用于線程執行臨界區比較快的場景,也就是獲得鎖后,快速釋放了鎖。

既想要自旋,又要避免無限制自旋,因此引入了自適應自旋:

    class MyLock {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        //最大自旋次數
        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++;
            }
        }
    }

可以看出,給自旋設置了最大自旋次數,若還是沒能獲取到鎖,則退出死循環。

實際上synchronized、ReentrantReadWriteLock 等的實現里,同樣為了盡量避免線程掛起/喚醒,在搶占鎖的過程中也是采用了自旋(自適應自旋)的思想,但這只是它們鎖實現的以小部分,它們并不是自旋鎖。

5、為什么需要等待/通知機制

先看獨占鎖的偽代碼:

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

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

Thread1、Thread2 互斥拿到鎖后各干各的,互不干涉,相安無事。
若是現在Thread1、Thread2 需要配合做事,如:

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

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

如上代碼,Thread1需要判斷flag == true才會往下運行,而這個值需要Thread2來修改,Thread1、Thread2 兩者間有協作關系。于是Thread1需要調用wait 釋放鎖,并阻塞等待。Thread2在Thread1釋放鎖后拿到鎖,修改flag,然后notify 喚醒Thread1(喚醒時機在Thread2執行完臨界區代碼并釋放鎖后)。Thread1 被喚醒后繼續搶鎖,然后判斷flag==true,繼續做事。
于是,Thread1、Thread2愉快配合完成工作。
為啥wait/notify 需要先獲取鎖呢?flag 是線程間共享變量,需要在并發條件下正確訪問,因此需要鎖。

至此,線程并發系列文章暫時告一段落了。大家對這系列文章有疑惑,請評論留言。

本文基于jdk 1.8。

您若喜歡,請點贊、關注,您的鼓勵是我前進的動力

持續更新中,和我一起步步為營系統、深入學習Android/Java

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容