理解java關鍵字Synchronized(學習筆記)


之前學習了線程的一些相關知識,今天系統的總結下來

目錄

1. Java對象在堆內存中的存儲結構

2. Monitor管程

3. synchronized鎖的狀態變換以及優化

4. synchronized的同步性和可見性

5. jvm調優參數設置

6. 總結

 

1.Java對象在堆內存中的存儲結構

要想明白synchronized,必須先搞清楚Java對象在堆中的內存區域

 

實例數據存放類的屬性信息及其父類的屬性信息,在JVM分配策略的影響下,相同的寬度的字段會被分配到一起

(longs和doubles寬度相同,shorts和chars寬度相同等),而且子類的數據可能會插入到父類的字段間隙。

 

填充數據:填充補齊的作用,HotSpot要求對象所占空間的大小必須為8的整數倍,

若對象的實例數據以及對象頭所占空間大小已經是8字節的整數倍,則該區域不會存在,否則補全。

 

對象頭:該區域是我們的重點,它主要分為兩個部分:運行時數據區MarkWord和類型指針以及數組長度(不一定存在)。

一.   運行時數據區保存了運行時對象的信息:HashCode,GC分代,GC標志,鎖狀態標志以及線程持有的鎖,偏向線程ID等信息。

二.   類型指針就是指向類的元數據指針,在Hotspot中該指針指向對象的類對象數據區(jdk1.8中方法區已經被替代成元數據區)。

三.   數組長度,若該對象為數組,還要保存數組的長度信息。

我們的重點是MarkWord數據區,該區域的數據結構不是固定的,一般大小為32bit/32位,64bit/64位。

下圖為64位下的MarkWord存儲結構。

偏向鎖標識位

鎖標識位

鎖狀態

存儲內容

0

01

未鎖定

hash code(31),年齡(4)

1

01

偏向鎖

線程ID(54),時間戳(2),年齡(4)

00

輕量級鎖

棧中鎖記錄的指針(64)

10

重量級鎖

monitor的指針(64)

11

GC標記

空,不需要記錄信息

2.Monitor管程

Synchronized在不同的情境下有四種狀態:無鎖,偏向鎖,輕量級鎖,重量級鎖。而四種方式的實現主要依靠monitor對象。Monitor是對象天生自帶的一個對象,它有多種實現方式,其中一個為對象創建同時也創建了monitor(也可能是在線程持有該鎖時創建),若一個線程持有了該對象的鎖,那么該對象的monitor對象狀態為鎖定狀態。

 

在openjdk中查看objectmonitor.hpp的源碼部分如下

ObjectMonitor() {
         _header       = NULL;
         _count        = 0;
         _waiters      = 0,//等待線程數
         _recursions   = 0;//重入次數
         _object       = NULL;//寄生的對象。
         _owner        = NULL;//指向獲得ObjectMonitor對象的線程
         _WaitSet      = NULL;//處於wait狀態的線程,會被加入到wait set;
         _WaitSetLock  = 0 ;
         _Responsible  = NULL ;
         _succ         = NULL ;
         _cxq          = NULL ;
         FreeNext      = NULL ;
         _EntryList    = NULL ;//處於等待鎖block狀態的線程,會被加入到entry list;
         _SpinFreq     = 0 ;
         _SpinClock    = 0 ;
         OwnerIsThread = 0 ;
         previous_owner_tid = 0;// 監視器前一個擁有者線程的ID
         }

 

對象鎖的線程狀態被記錄在該對象中。

我們只看對象封裝的幾個重要字段:

_owner記錄當前獲取該對象鎖的線程。

_WaitSet:當前正在等待獲取該鎖處於阻塞狀態的線程集合(Set保證等待中的線程不可重復)

_EntryList: 當前處於等待狀態的線程處於該集合中。

_count: 記錄了持有該鎖的線程數,若一個線程獲取了該對象鎖,則計數器+1,執行wait方法后該對象減1,

同時 _owner對象被置位NULL,代表此時沒有線程持有該鎖。

 

3.synchronized鎖的狀態變換以及優化

 

在jdk1.6之后,java的鎖就進行了一系列的優化以解決資源搶占以及程序執行效率問題。

 

鎖的膨脹方向:無鎖->偏向鎖->輕量級鎖->重量級鎖

 

 

鎖狀態的詳情如下:

 

無鎖:共享數據 沒有沒任何線程所占用。

 

 

偏向鎖:大部分情況下,鎖不存在競爭,總是由同一線程多次獲取。當鎖在第一次被線程所獲取的時候,標志位變為01(偏向模式),同時進行CAS操作獲取當前線程的id記錄到markword中,若CAS操作成功,那么線程在此進入同步代碼塊且線程id已經和markword中的一致時,則不需要任何同步操作(locking,unlocking,update等),注意是不會有任何同步操作。若當有另一個線程嘗試獲取該鎖時,則宣布偏向模式結束,鎖狀態將會恢復為01(為鎖定)或00(輕量級鎖)。

 

 

 

輕量級鎖:在方法進入同步方法時,若此時同步方法沒有被鎖定,那么(標志位01),虛擬機會在當前線程所在方法的棧幀中開辟一個空間用來保存鎖記錄(Lock Record),用於存儲當前鎖所在的對象的MarkWord的拷貝,在搶占資源時,jvm會進行CAS操作進行失敗重試讓當前鎖所在對象的MarkWord更新為指向Lock Record的指針,若更新成功,代表搶占鎖成功,MarkWord變為00,表示處於輕量級鎖狀態。若更新失敗,jvm會先檢查當前鎖所在對象的MarkWord是否已經指向了線程的棧幀,若已經指向,則說明鎖已經被當前線程所持有,那么久可以進入同步塊;若沒有指向當前線程的棧幀,就說明有至少兩個線程在搶占同一把鎖,則該鎖會膨脹為重量級鎖。鎖標志位變為10,后面搶占的線程若沒有獲取到該鎖就得進入阻塞狀態了。

 

 

 

偏向鎖和輕量級鎖的區別:兩中狀態非常相似,但在無競爭的情況下卻有區別:輕量級鎖在無競爭情況下是通過CAS操作進行失敗重試來消除鎖,而偏向鎖在無競爭情況下直接取消了CAS。

 

 

 

自旋鎖(jdk1.4引入): 在只有兩個線程爭搶同一把鎖的情境下,在線程A試圖進入同步方法或者同步代碼塊時,若該同步方法或同步代碼塊已經被線程B搶先進入,那么此時線程A需要掛起,直到其他線程B執行完才能恢復繼續進行鎖的搶占,但是大部分情況下鎖被某個線程持有只持續很短的時間(單次持有鎖到釋放鎖所消耗的時間),所以線程A在很短的時間內掛起再恢復是不值得的,於是就讓線程A在B持有鎖之后不釋放CPU資源,而是繼續循環等待鎖,直到獲取到鎖,稱為自旋。適用於線程單次持有鎖到釋放同一把鎖所消耗的時間很短的情況,否則其他線程長時間循環等待鎖,不釋放cpu資源只會更加消耗資源。

 

 

 

自適應自旋鎖(jdk1.6引入):為了解決自旋鎖所帶來的可能出現的問題,此時一把鎖的在等待其他線程釋放鎖時,自旋的次數由前一次上持有該鎖的自旋時間以及當前持有該鎖的線程的狀態來決定。

 

 

鎖的去除優化

 

在JIT編譯時,jvm會進行掃描,去除不可能存在競爭情況的鎖。看一個例子:

public class Sync{ private String name; public Sync(String name){ this.name = name; }   public synchronized String changeName(String name){ this.name = name; return name; } } Public class Test{ Public void test(String name){ Sync sync = new Sync(); Sync.changeName(“John”); } public static void mian(String[] args){ Test test = new Test(); for(int i = 0; i < 100; i++){ test.test(“Jack”); } } } 

 

 

 

在上邊這個例子中,jvm會進行JIT優化,去除synchronized鎖,因為sycn對象生存周期始終在java虛擬機棧中,不可能存在鎖競爭的情況。

 

鎖的去除優化

public static String test2(String name){ Sync sync = new Sync(“Tom”); for(int i = 0 ; i< 100 ; i++){ sync.changeName(name); } }

在上邊這個例子中,由於changeName是同步方法,在這個for循環中,每次進入和退出循環都要進行lock和unlock操作,但是這是沒有必要的操作,於是jvm會將synchronized加到循環的外邊,只進行一次lock和unlock操作。

 

4.synchronized的同步性和可見性

 

同步性:synchronized修飾的方法或者代碼塊只允許一個線程進入,只有當該線程退出同步區域時,才允許下一個線程進入。

 

可見性:當線程釋放鎖是,當前線程會把本地內存中的共享變量立即刷新到主內存中。保證其他線程獲取該共享變量的鎖時,獲取到的是共享變量的最新值。而當線程獲取鎖時,當線程對共享變量的拷貝會被置為無效,強制當前線程的共享變量是從主內存中拿到的最新值,從而保證可見性。

 

5.jvm調優參數設置

自旋鎖:-XX:PreBlockSpin 來更改自旋的次數,默認為自旋10次。由於jdk1.6只有加入了自適應的自旋鎖,自旋鎖會自己判斷自旋鎖的自旋次數,更智能。

偏向鎖:-XX:UseBiasedLocking 來設置是否啟用偏向鎖。-XX:BiasedLockingStartupDelay 來設置java程序啟動后延遲開啟偏向鎖的時間

6.總結

通過synchronized的底層原理可以了解到synchronized是如何保證同步和共享變量的,以及在具體到代碼層面時,jvm又是如何進行進行優化的,以及在不同的場景下如何進行參數調優。

 

 

 

閱讀參考書籍《深入理解jvm》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM