一文帶你理解Java中Lock的實現原理

當多個線程須要訪問某個公共資源的時候,咱們知道須要經過加鎖來保證資源的訪問不會出問題。java提供了兩種方式來加鎖,一種是關鍵字:synchronized,一種是concurrent包下的lock鎖。synchronized是java底層支持的,而concurrent包則是jdk實現。關於synchronized的原理能夠閱讀再有人問你synchronized是什麼,就把這篇文章發給他。java

在這裏,我會用盡量少的代碼,儘量輕鬆的文字,儘量多的圖來看看lock的原理。安全

咱們以ReentrantLock爲例作分析,其餘原理相似。數據結構

我把這個過程比喻成一個作菜的過程,有什麼菜,作法如何?ide

我先列出lock實現過程當中的幾個關鍵詞:計數值、雙向鏈表、CAS+自旋函數


使用例子

 

import java.util.concurrent.locks.ReentrantLock;

public class App {


    public static void main(String[] args) throws Exception {

        final int[] counter = {0};


        ReentrantLock lock = new ReentrantLock();


        for (int i= 0; i < 50; i++){

            new Thread(new Runnable() {

                @Override

                public void run() {

                    lock.lock();

                    try {

                        int a = counter[0];

                        counter[0] = a + 1;

                    }finally {

                        lock.unlock();

                    }

                }

            }).start();

        }


        // 主線程休眠,等待結果

        Thread.sleep(5000);

        System.out.println(counter[0]);

    }

}

在這個例子中,開50個線程同時更新counter。分紅三塊來看看源碼(初始化、獲取鎖、釋放鎖)優化


實現原理

ReentrantLock() 幹了啥ui

 
 /**      * Creates an instance of {@code ReentrantLock}.      * This is equivalent to using {@code ReentrantLock(false)}.      */

    public ReentrantLock() {

        sync = new NonfairSync();

    }

在lock的構造函數中,定義了一個NonFairSync,url

static final class NonfairSync extends Sync  

NonfairSync 又是繼承於Syncspa

abstract static class Sync extends AbstractQueuedSynchronizer 

一步一步往上找,找到了
這個鬼AbstractQueuedSynchronizer(簡稱AQS),最後這個鬼,又是繼承於AbstractOwnableSynchronizer(AOS),AOS主要是保存獲取當前鎖的線程對象,代碼很少再也不展開。
最後咱們能夠看到幾個主要類的繼承關係。線程

f7ec178b93eeb15cc98dfac8732fea2159b79358

FairSync 與 NonfairSync的區別在於,是否是保證獲取鎖的公平性,由於默認是NonfairSync,咱們以這個爲例瞭解其背後的原理。

其餘幾個類代碼很少,最後的主要代碼都是在AQS中,咱們先看看這個類的主體結構。

AbstractQueuedSynchronizer是個什麼

 

57b7475d527d592e0c626776bf1238b76fa375cf


再看看Node是什麼?

831781ec10901a1060ba59cb8947f7df339c15d9


看到這裏的同窗,是否是有種熱淚盈眶的感受,這尼瑪,不就是雙向鏈表麼?我還記得第一次寫這個數據結構的時候,發現竟然還有這麼神奇的一個東西。

最後咱們能夠發現鎖的存儲結構就兩個東西:"雙向鏈表" + "int類型狀態"。
須要注意的是,他們的變量都被"transientvolatile修飾。

01ceee2dbde0eae7b901ab3bee596ad38866757c

一個int值,一個雙向鏈表是如何烹飪處理鎖這道菜的呢,Doug Lea大神就是大神,咱們接下來看看,如何獲取鎖?

lock.lock()怎麼獲取鎖?
/**  * Acquires the lock.  */
public void lock() {

    sync.lock();

}

能夠看到調用的是,NonfairSync.lock()

5a7809035c292cf28cfb0eebd8202b11038c9f2c


看到這裏,咱們基本有了一個大概的瞭解,還記得以前AQS中的int類型的state值,這裏就是經過CAS(樂觀鎖)去修改state的值。lock的基本操做仍是經過樂觀鎖來實現的

獲取鎖經過CAS,那麼沒有獲取到鎖,等待獲取鎖是如何實現的?咱們能夠看一下else分支的邏輯,acquire方法:

public final void acquire(int arg) {

    if (!tryAcquire(arg) &&

        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

        selfInterrupt();

}

這裏幹了三件事情:

  • tryAcquire:會嘗試再次經過CAS獲取一次鎖。

  • addWaiter:將當前線程加入上面鎖的雙向鏈表(等待隊列)中

  • acquireQueued:經過自旋,判斷當前隊列節點是否能夠獲取鎖。


addWaiter 添加當前線程到等待鏈表中 811ad99c49d08e940aeacd7becc8cb98e59cf190


能夠看到,經過CAS確保可以在線程安全的狀況下,將當前線程加入到鏈表的尾部。
enq是個自旋+上述邏輯,有興趣的能夠翻翻源碼。

acquireQueued

c14e8bb94f0218794ac03e87c2e556eabd2451aa


能夠看到,噹噹前線程到頭部的時候,嘗試CAS更新鎖狀態,若是更新成功表示該等待線程獲取成功。從頭部移除。

 

71e8b71038243dfaf21ebcf6f9fcc5fbaa659b08

最後簡要歸納一下,獲取鎖的一個流程

48c01d6093d60683cf2d842a26d7e407cfbcde7f

lock.unlock() 釋放鎖

 

public void unlock() {

    sync.release(1);

}

能夠看到調用的是,NonfairSync.release()

7330906450a4f71f1d693be4a8c7b742d8466c84


最後有調用了NonfairSync.tryRelease()

f69fb329fa6298b88109b96e611db6ff925b445f


基本能夠確認,釋放鎖就是對AQS中的狀態值State進行修改。同時更新下一個鏈表中的線程等待節點。


總結
  • lock的存儲結構:一個int類型狀態值(用於鎖的狀態變動),一個雙向鏈表(用於存儲等待中的線程)

  • lock獲取鎖的過程:本質上是經過CAS來獲取狀態值修改,若是當場沒獲取到,會將該線程放在線程等待鏈表中。

  • lock釋放鎖的過程:修改狀態值,調整等待鏈表。

  • 能夠看到在整個實現過程當中,lock大量使用CAS+自旋。所以根據CAS特性,lock建議使用在低鎖衝突的狀況下。目前java1.6之後,官方對synchronized作了大量的鎖優化(偏向鎖、自旋、輕量級鎖)。所以在非必要的狀況下,建議使用synchronized作同步操做。

最後,但願個人分析,能對你理解鎖的實現有所幫助。

相關文章
相關標籤/搜索