synchronized底層實現原理與ReentrantLock初步理解

前言:在慕課網上學習劍指Java面試-Offer直通車時所做的筆記,供本人複習之用,比較難,我也沒大懂,只記錄大概意思以後有接觸了再改,想要詳細解說的不建議看這篇博客.

目錄

第一章 對象在內存中的佈局

第二章 Monitor

2.1 Monitor在字節碼中的表示

第三章 鎖的優化

3.1 自旋鎖與自適應自旋鎖

3.1.1 自旋鎖

3.1.2 自適應自旋鎖

3.2 鎖消除

3.3 鎖粗化

第四章 synchronized的四種狀態

4.1 偏向鎖

4.2 輕量級鎖

第五章 Synchronized和ReentrantLock的區別

5.1 ReentrantLock公平性設置

5.2 wait/notify/notifyAll對象化


第一章 對象在內存中的佈局

HotSpot 對象在內存中的佈局分爲三塊區域,對象頭,實例數據,對齊填充,我們在這裏主要講解對象頭.

synchronized使用的鎖對象是存儲在java對象頭裏的,其主要結構是由Mark Word和Class Metadata Address組成,Class Metadata Address是指向類元數據的指針,虛擬機通過這個指針確認其是哪個對象的實例,Mark Word用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵.

 Mark Word:

 

第二章 Monitor

上面介紹了java對象頭,下面我們介紹Monitor.

Monitor:每個對象天生自帶了一把看不見的鎖,叫做內部鎖或者Monitor鎖.

Monitor也稱爲管程或者監視器鎖,我們可以把它理解爲一個同步工具,也可以描述爲一種同步機制,通常它被描述爲一個對象.

這裏我們拿重量級鎖進行分析,鎖的標識位爲10,指針指向的是monitor對象的起始地址,每個對象都存在着一個monitor與之關聯,monitor存在於對象的對象頭中,對象與monitor之間有多存在多種實現方式,如monitor可以與對象一起存在銷燬,或當線程試圖獲取對象鎖時,自動生成.當monitor被某個線程持有後,它便處於鎖定狀態.

在java虛擬機HotSpot中monitor由ObjectMonitor來實現,位於HotSpot虛擬機源碼objectMonitor裏,通過C++來實現.

        ObjectMonitor中有兩個隊列,一個是WaitSet一個是EntryList,可以與等待池與鎖池聯繫起來,他們就是用來保存ObjectWaiter的對象列表,每個對象鎖的線程都會被封裝成ObjectWaiter來保存到裏面,owner指向持有ObjectMonitor的線程.

        當多個線程訪問同一段同步代碼的時候,首先會進入到EntryList集合中,當線程獲取到對象的Monitor之後,就進入到Object區域,並把Monitor中的Owner變量設置爲當前線程,同時Monitor中的count就會加1,如果線程調用wait方法將會釋放當前持有的Monitor,owner會被恢復成null,count也會被減1,同時該線程ObjectWaiter實例就會進入到waitSet集合等待被喚醒,若當前線程執行完畢,它也將釋放monitor鎖,並復位對應變量的值,以便其它線程進入獲取monitor鎖.

Monitor鎖的競爭,獲取與釋放.

 

2.1 Monitor在字節碼中的表示

java代碼:

對應的字節碼:

syncsTask方法字節代碼:

可以分析出,同步語句塊的實現依賴的是monitorenter與monitorexit指令.

monitorenter指向代碼塊的開始位置,首先獲取printStream這個類,傳入參數,執行方法.

monitorexit指明同步代碼塊的結束位置.

當執行monitorenter指令時,當前線程將試圖獲取對象鎖即Object(沒聽清,音譯)所對應的持有權,當Object的Monitor進入計數器的count爲0時,線程就可以成功的獲取到Monitor,並將計數器設置爲1表示取鎖成功,如果我們當前線程在之前已經擁有了objectMonitor的持有權,它可以重入這個monitor.假如其它線程已經先於當前線程擁有ObjectMonitor的所有權,那麼當前線程將會被阻塞在這裏,直到持有該鎖的線程執行完畢,即monitorexit被執行,執行線程將釋放Monitor鎖,並設置計數器爲0,其它線程將有機會持有Monitor.

爲了保證該方法異常時也能正確執行,編譯器會自動生成一個異常處理器,包含另一個monitorexit方法.

什麼是重入:

 重入代碼如下:

 

syncTask方法字節代碼:

可以看到它並沒有monitorenter和monitorexit,字節碼也比較短,因爲方法集的同步時隱式的,即無需通過字節碼指令來控制.

我們可以看到有ACC_SYNCHORONIZED一個訪問標誌,用來區分一個方法是否是同步方法,當方法調用時此訪問標誌會被檢測,當被設置時線程將會持有monitor,然後再執行方法,最後在方法無論是正常還是異常完成的情況下執行monitorexit.

第三章 鎖的優化

爲什麼會對synchronized嗤之以鼻?

java6之後,synchronized性能得到了極大的提升.

 

3.1 自旋鎖與自適應自旋鎖

3.1.1 自旋鎖

許多情況下,共享數據的鎖定狀態持續時間短,切換線程不值.

可以讓沒獲取到monitor鎖的線程在門外等待一會兒,但不放棄CPU的執行時間.即通過讓線程執行忙循環(自旋)等待鎖的釋放,不讓出CPU.不像sleep一樣會讓出CPU的執行時間.

缺點:若鎖被其它線程長時間佔用,會白白等很久,會帶來許多性能上的開銷.所以可以設置如果在一定時間內沒有等到鎖,就應該掛起線程了.

3.1.2 自適應自旋鎖

由於每次線程需要等待的時間不是固定的,所以想設定時間比較合理是很困難的,所以我們需要自適應自旋鎖.

自旋的次數不再固定,由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定.

如果對於某個鎖,自旋經常獲取到鎖,那麼就增加等待時間,如果對於某個鎖,自旋很少獲取到鎖,那在以後獲取這個鎖時可能省略到這個過程,以避免浪費時間.

 

3.2 鎖消除

java 虛擬機在JIT編譯時,對運行上下文進行掃描,去除不可能存在競爭的鎖.

可以看到appen是加鎖的,但是這個鎖永遠不會發生競爭,sb屬於本地變量沒有return,所以這個鎖可以消除.

 

 

3.3 鎖粗化

我們有時會將鎖限制在儘量小的範圍,這樣需要同步的數量儘可能的變小,使等待鎖的線程儘可能塊的拿到鎖.

但是如果一連串系類操作都會同一個對象反覆加鎖和解鎖,甚至加鎖操作出現在循環體中,那即使沒有鎖競爭,頻繁的進行互斥鎖操作也會導致不必要的性能浪費,此時我們可以礦大枷鎖的範圍,避免反覆加鎖和解鎖,

如下圖所示,JVM會把加鎖的範圍到循環的外部,使整個操作只需要加鎖一次.

 

第四章 synchronized的四種狀態

無鎖就是沒有鎖,重量級鎖我們剛纔說過,所以我們這裏主要介紹偏向鎖和輕量級鎖.

4.1 偏向鎖

應用在鎖不存在多線程競爭,總是由同一線程多次獲得.

不適用於鎖競爭比較激烈的多線程場合.

4.2 輕量級鎖

偏向鎖失敗後會膨脹爲輕量級鎖.

具體過程:

解鎖的過程:

鎖的內存語義:

總的來說:

 

總結:對象頭裏有mark word,markword裏存儲着這個鎖的狀態,

當多個線程競爭一個鎖時,有幾種不同的情況:

偏向鎖,

輕量級鎖,

重量級鎖

 

第五章 Synchronized和ReentrantLock的區別

在java5以前,Synchronized是僅有的同步手段,從java5開始,提供了ReentrantLock(再入鎖)的實現,它的語義和synchronized基本相同,通過代碼直接調用lock方法去獲取,代碼編寫也更加靈活.

ReentrantLock的特點:

查看ReentrantLock源碼,進入其lock方法中.

繼續點進其acquire方法中,再點進其acquireQueued方法中.

我們發現acquireQueued在類AbstractQueuedSynchronizer中,AbstractQueuedSynchronizer是隊列同步器,簡稱AQS,它是java併發用來構建鎖或其它同步組件的基礎框架,是JUCpacakge的核心,一般使用AQS的方式是繼承.

像ReentrantLock這些子類是通過AQS實現的抽象方法來管理這些同步狀態,一種同步結構往往是可以利用其它的結構去實現的,但是對某種同步結構的傾向會導致複雜晦澀的實現邏輯,所以把基礎的同步相關的操作抽象在AQS中了,利用AQS爲我們提供同步結構實現了範本.

AQS內部的數據和方法可以簡單拆分爲一個volatile數組成員表徵狀態即state,一個先進先出的等待隊列,各種針對CAS的基礎操作方法,以及期望各種同步結構去實現的acquire,release方法,利用AQS實現一個同步結構至少要實現兩個基本類型的方法,acquire操作,獲取資源的獨佔權,release操作,用來釋放對某個資源的獨佔.

 

5.1 ReentrantLock公平性設置

傳入true表示是公平鎖.

建議程序確實有公平需要的時候再用,不用會有額外開銷.

具體代碼:

public class ReentrantLockDemo implements  Runnable{
    private static ReentrantLock lock = new ReentrantLock(true);
    @Override
    public void run(){
        while (true){
            try{
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " get lock");
                Thread.sleep(1000);
            } catch (Exception e){
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo rtld = new ReentrantLockDemo();
        Thread thread1 = new Thread(rtld);
        Thread thread2 = new Thread(rtld);
        thread1.start();
        thread2.start();
    }
}

公平鎖輸出:

非公平鎖輸出:

ReentrantLock相比Synchronized因爲可以像普通對象一樣使用,所以可以利用它來提供各種便利的方法,進行精細的同步操作.甚至可以實現Synchronized難以表達的用例.

5.2 wait/notify/notifyAll對象化

ReentrantLock將Synchronized轉變成了可控的對象,是不是也能將前面講的wait/notify/notifyAll對象化?

位於JUC包中的locks.Condition做到了這一點.Condition最爲典型的應用場景就是標準類庫中的ArrayBlockingQueue,ArrayBlockingQueue是數組實現的線程安全的,有界的阻塞隊列,線程安全是指ArrayBlockingQueue內部通過互斥鎖保護競爭資源,其互斥鎖是通過ReentrantLock來實現的,實現了多線程對競爭資源的互斥訪問,而有界則指的是ArrayBlockingQueue對應的數組是有界限的,阻塞隊列是指多線程訪問競爭資源時,當競爭資源已被某線程獲取時,其它要獲取該資源的線程要阻塞等待.

ArrayBlockingQueue與condition是組合的關係,ArrayBlockingQueue內部有兩個condition對象,一個是notEmpty,一個是notFull,而且condition依賴於ArrayBlockingQueue存在,通過condition可以實現對ArrayBlockingQueue更精確的訪問,

可以看其構造函數,notEmpty與notFull都是同一個lock創建出來的,然後使用在特定的操作中.

notEmpty用在take方法中,用在當count=0的時候,滿足當隊列爲空時,試圖take線程的正確行爲,應該是等待有新的消息加入到隊列纔去做返回,如同之前的future,使用notEmpty就可以優雅的實現等待邏輯,take操作的前提是要保證消息入隊,只有隊列有消息才觸發去取走.

看一下入隊的方法.可以看到一旦有消息被放入隊列當中,count便會++,notEmpty會調用signal函數去通知等待的線程,signal如同之前說的notify,此時非空條件就會滿足,take就能取到對應的東西了.

通過signal和await的組合,ArrayBlockingQueue就能優雅的完成了條件判斷和通知等待線程,非常順暢完成了狀態流轉.

注意到notEmpty是從new condition出來的,發現condition來自於ConditionObject,ConditionObject來源於AbstractQueuedSynchronizer也就是AQS框架,就是將我們的wait,notify,notifyall等操作轉換成了相對應的對象,將複雜而晦澀的同步操作轉變爲直觀可控的對象行爲.

總結:

針對最後一條我們去找到park方法.

首先找到ReentrantLock類的lock方法,點擊進acquire方法中.再點擊進acquireQueued方法,

在acquireQueued中找到parkAndCheckInterrupt方法

點擊進去,發現其調用的是LockSupport方法,再點進去發現其調用的是U.park方法.

再點進去,最後發現park方法位於unsafe類裏,unsafe是一個類似於後門的工具,可以在任意內存位置處讀寫數據.另外unsafe還支持一些CAS的操作.

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章