Synchronized原理以及Java鎖膨脹

Synchronized原理以及Java鎖膨脹

 

從語法上講,Synchronized可以把任何一個非null對象作爲"鎖",在HotSpot JVM實現中,鎖有個專門的名字:對象監視器(Object Monitor)。
1、Synchronized屬性:原子性,可見性,有序性
(1)原子性:確保線程互斥的訪問同步代碼;
爲什麼volatile已經保證了變量的可見性,synchronized依然保證變量的可見性:
(2)可見性:保證共享變量的同步能夠及時可見,通過JMM模型:“對一個共享變量unlock(解鎖)之前一定要先同步到主內存中;如果要是對一個共享變量進行lock(鎖定)的之前一定要清除工作內存中的值,然後通過主內存中的值加載到工作內存中”;
(3)有序性:有效解決重排序問題,即 “一個unlock操作先行發生(happen-before)於後面對同一個鎖的lock操作”;
2、同步的原理:
(1)方法同步:JVM從方法常量池的表(method_info Structure)中,獲取ACC_SYNCHRONIZED訪問標誌來區分一個方法是否是同步方法,如果設置了該標識。當方法被調用時,執行線程將先持有monitor,然後執行方法,待方法執行完畢後釋放monitor;
public synchronized void f(){ //這個是同步方法 System.out.println("Hello world"); }
字節碼文件:

(2)代碼塊同步:代碼塊的同步利用兩個字節碼文件:monitorenter和monitorexit這兩個字節碼指令。它們分別位於代碼塊的開始和結束位置。當jvm執行到monitorenter指令時,當前線程獲取monitor的持有權,就把鎖的計數器+1;當jvm執行到monitorexit指令時,鎖計數器-1;當計數器爲0的時候,鎖釋放;
public void g(){
//這個是同步代碼塊 synchronized (this){ System.out.println("Hello world"); } }

3、由Java對象的內部存儲引發的synchronized概念:
在JVM中Java對象主要由3部分組成:對象頭,實例數據,對齊填充;

  1. 實例數據:存放類的屬性數據信息,包括父類的屬性信息;
  2. 對齊填充:由於虛擬機要求 對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊;
  3. 對象頭:Java對象頭一般佔有2個機器碼(在32位虛擬機中,1個機器碼等於4字節,也就是32bit,在64位虛擬機中,1個機器碼是8個字節,也就是64bit),但是 如果對象是數組類型,則需要3個機器碼,因爲JVM虛擬機可以通過Java對象的元數據信息確定Java對象的大小,但是無法從數組的元數據來確認數組的大小,所以用一塊來記錄數組長度。

 

4、鎖消除:
爲了保證數據的完整性,在進行操作時需要對這部分操作進行同步控制,但是在有些情況下,JVM檢測到不可能存在共享數據競爭,這是JVM會對這些同步鎖進行鎖消除。
鎖消除的依據是逃逸分析的數據支持
如果不存在競爭,爲什麼還需要加鎖呢?所以鎖消除可以節省毫無意義的請求鎖的時間。變量是否逃逸,對於虛擬機來說需要使用數據流分析來確定,但是對於程序員來說這還不清楚麼?在明明知道不存在數據競爭的代碼塊前加上同步嗎?但是有時候程序並不是我們所想的那樣?雖然沒有顯示使用鎖,但是在使用一些JDK的內置API時,如StringBuffer、Vector、HashTable等,這個時候會存在隱形的加鎖操作。比如StringBuffer的append()方法,Vector的add()方法:
public void vectorTest(){ Vector<String> vector = new Vector<String>(); for(int i = 0 ; i < 10 ; i++){ vector.add(i + ""); } System.out.println(vector); }
在運行這段代碼時,JVM可以明顯檢測到變量vector沒有逃逸出方法vectorTest()之外,所以JVM可以大膽地將vector內部的加鎖操作消除。

5、鎖粗化:
在使用同步鎖的時候,需要讓同步塊的作用範圍儘可能小—僅在共享數據的實際作用域中才進行同步,這樣做的目的是 爲了使需要同步的操作數量儘可能縮小,如果存在鎖競爭,那麼等待鎖的線程也能儘快拿到鎖。
在大多數的情況下,上述觀點是正確的。但是如果一系列的連續加鎖解鎖操作,可能會導致不必要的性能損耗,所以引入鎖粗話的概念。
鎖粗話概念比較好理解,就是將多個連續的加鎖、解鎖操作連接在一起,擴展成一個範圍更大的鎖
如上面實例:
vector每次add的時候都需要加鎖操作,JVM檢測到對同一個對象(vector)連續加鎖、解鎖操作,會合並一個更大範圍的加鎖、解鎖操作,即加鎖解鎖操作會移到for循環之外。
6、偏向鎖VS無鎖VS輕量級鎖VS重量級鎖(Java鎖的膨脹過程和優化):
(1)markword的存儲結構:
在32虛擬機下:

在64位虛擬機下:

後兩位的鎖標誌位:無鎖:01;偏向鎖:01;

Lock Record是線程私有的數據結構,每一個線程都有一個可用Lock Record列表,同時還有一個全局的可用列表。每一個被鎖住的對象Mark Word都會和一個Lock Record關聯(對象頭的MarkWord中的Lock Word指向Lock Record的起始地址),同時Lock Record中有一個Owner字段存放擁有該鎖的線程的唯一標識(或者object mark word),表示該鎖被這個線程佔用。如下圖所示爲Lock Record的內部結構:

Lock Record

描述

Owner

初始時爲NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程唯一標識,當鎖被釋放時又設置爲NULL;

EntryQ

關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程;

RcThis

表示blocked或waiting在該monitor record上的所有線程的個數;

Nest

用來實現 重入鎖的計數;

HashCode

保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。

Candidate

用來避免不必要的阻塞或等待線程喚醒,因爲每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然後因爲競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。

(2)偏向鎖:HotSpot人員發現大多數情況下雖然加了鎖,但是同一線程卻總是能反覆獲取到鎖:當一個線程訪問同步塊且獲取到鎖的時候,會在對象頭和棧幀記錄存儲取得偏向鎖的線程ID,下一次有線程嘗試去獲取鎖的時候,首先檢查這個對象頭MarkWord是不是存儲着這個線程的ID,如果是,那麼直接進去不需要任何操作。

(3)偏向鎖存在的意義(即偏向鎖解決的問題):
SMP(對稱多處理器)架構:
其意思就是CPU會通過一條消息總線(BUS),靠此主線來連接各個內核之間的通信關係:

結構如下:假如爲6核CPU,每個core(內核)前都會有一個cache,例如,Core1 和 Core2都會通過總線(Bus)來加載數據到cache中,之後同步到core中,但是當Core1 修改L1 Cache這個位置的數據時,使Core 2中的L1 Cache中的數據失效,而Core2一旦發現自己L1 Cache中的值失效(稱爲Cache命中缺失)則會通過總線從內存中加載該地址最新的值,大家通過總線的來回通信稱爲“Cache一致性流量”;如果Cache一致性流量過大,就會造成總線成爲瓶頸,而CAS操作通過這種不斷比對的過程,恰好會導致Cache一致性流量過大,從而引起“總線風暴”,這就是所謂的本地延遲,本質上偏向鎖就是爲了消除CAS,降低Cache的一致性流量;

當一個線程訪問同步塊並獲取鎖(用synchronized鎖定的任何對象,方法都被成爲 “鎖” )時,會將該線程的ID存儲到對象頭和棧幀的鎖記錄裏,以後該線程進入和退出同步塊時不需要進行CAS操作,執行的過程如下:
(1)檢測Mark Word中是否有偏向鎖1,鎖標誌位01;
(2)如果有則檢測線程ID是否是當前線程ID,
1)如果是執行同步代碼塊;
2)如果不是,則通過CAS操作競爭鎖,競爭成功,則將該線程ID替換原有鎖記錄裏的線程ID;
3)如果競爭鎖失敗(說明原有線程依然持有鎖),偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼塊;
偏向鎖的釋放也叫鎖撤銷,步驟如下:
1、暫停擁有偏向鎖的線程;
2、判斷鎖定對象是否擁有鎖,否,則恢復到無鎖狀態(01),是,則掛起持有鎖的當前線程,並將指向當前線程鎖記錄的地址指針存放到對象頭Mark Word中,升級爲輕量鎖(00),然後恢復持有鎖的當前線程,進入到輕量級鎖的競爭中;

線程安全(中)--徹底搞懂synchronized(從偏向鎖到重量級鎖)

(4)輕量級鎖:當關閉偏向鎖或者存在多個線程競爭就會導致偏向鎖升級爲輕量級鎖;
獲取輕量級鎖的步驟如下:
1、當線程進入到同步塊時,如果同步對象鎖狀態爲無鎖狀態(偏向鎖:0,鎖的標誌位:01),虛擬機會首先在當前線程的棧幀中,建立一個LockRecord鎖的空間,用於存儲Mark Word中的拷貝,此時線程堆棧與對象頭的狀態如下圖所示:

2、將Mark Word中的數據拷貝到鎖記錄(Lock Record)中;
3、拷貝成功以後,虛擬機將會使用CAS操作將Mark Word中的Lock Word指針指向此時Lock Record中地址,並將Lock Record中的Owner指針指向Object Mark Word:
(1)如果更新成功:說明當前線程獲取到了輕量級鎖,同時JVM會將該對象的Mark Word中的鎖標誌位更新爲“00”;
(2)如果更新失敗:JVM會檢查該對象的Mark Word中的Lock Record指針是否指向Lock Record,如果是,則說明持有了鎖(可以繼續執行同步代碼塊),如果不是,說明有多個線程競爭鎖,輕量級鎖上升爲重量級鎖,鎖的標誌位爲“10”;

輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:
1、通過CAS操作嘗試把線程中複製的Displaced Mark Word對象替換當前的Mark Word;
2、如果替換成功,整個同步過程就完成了,恢復到無鎖狀態(01);
3、如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程;

(5)重量級鎖:
Synchronized是通過對象內部的一個叫做 監視器鎖(Monitor)來實現的。但是監視器鎖本質又是依賴於底層的操作系統的Mutex Lock來實現的。而操作系統實現線程之間的切換這就需要從用戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是爲什麼Synchronized效率低的原因。因此,這種依賴於操作系統Mutex Lock所實現的鎖我們稱之爲 “重量級鎖”。

 

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