文章目錄
- 前言
- 一、認識synchronized
- 二、Synchronized原理分析
-
- 2.1、對象在記憶體中的布局
-
- 介紹對象的三部分
- Java對象頭(四種狀态,重要)
- 2.2、JDK1.6之後鎖更新(過程)
- 總結
- 三、同步方法、代碼塊反編譯
- 參考文章
前言
本章節講述了
synchronized
的相關原理分析,包含鎖更新的過程。
部落客文章彙總:部落格目錄索引(持續更新)
一、認識synchronized
多線程并發程式設計中,
synchronized
關鍵字常用于來保證多線程情況下執行代碼的同步,其一直是元老級别,許多人也稱之為
重量級鎖
。在
java SE 1.6
之後對該關鍵字進行了各種優化,在
java SE 1.6
中為了減少獲得鎖與釋放鎖帶來的性能消耗引入了偏向鎖和輕量級鎖,根據不同的競争情況會進行對
synchronized
鎖進行更新!
- JDK1.6前,稱之為重量級鎖。
- JDK1.6後,
有一個鎖更新過程。無鎖->偏向鎖->輕量級鎖->重量級鎖。Synchronized
看一下
synrhonzied
應用的不同分類情況:
二、Synchronized原理分析
2.1、對象在記憶體中的布局
介紹對象的三部分
在
Hotspot
虛拟機(現在jvm使用的虛拟機)中,對象在記憶體中的存儲布局,可分為三個區域:對象頭(Header)、執行個體資料(Instance Data)、對其填充(Padding)。一般而言,
synchronized
使用的鎖對象(
monitor
)是存儲在
Java
對象頭中,其實輕量級鎖與偏向鎖的關鍵。
- 一個對象執行個體包含這三個部分。
對象頭:對象頭又可以分為兩塊内容
- 第一部分用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分資料的長度在32位和64位的虛拟機中分别位32bit和64bit,官方稱它為
。Mark Word
- 另一部分是類型指針,指向它的類中繼資料的指針,用于判斷對象屬于哪個
的執行個體,另外,如果對像是一個數組,那在對象頭中還必須有一塊用于記錄數組長度的資料,因為虛拟機可以通過普通Java對象的中繼資料資訊确定Java對象的大小,但是從數組的中繼資料中卻無法确定數組的大小。Class
執行個體資料:
- 執行個體資料部分是對象真正存儲的有效資訊,也是在程式代碼中所定義各種類型的字段内容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄下來。父類定義的變量會出現在子類定義的變量的前面。各字段的配置設定政策為longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同寬度的字段總是被配置設定到一起,便于之後取資料。
對齊填充:
- 對齊填充并不是必然存在的,它僅僅起着占位符的作用。為什麼需要有對齊填充呢?由于
的自動記憶體管理系統要求對象起始位址必須是8位元組的整數倍,換句話,就是對象的大小必須是8位元組的整數倍。而對象頭正好是8位元組的倍數。是以,當對象執行個體資料部分沒有對齊時,就需要通過對齊填充來補全。Hotspot VM
Java對象頭(四種狀态,重要)
在對象頭中主要包括兩個部分資料:
Mark Word
(标記字段)、
Class Poniter
(類型指針)
- 其中的
用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程的ID、偏向時間戳等,該标記字段是實作輕量級鎖和偏向鎖的關鍵。Mark Word
下面是32位與64位對象頭(
MarkWord
):
說明:上面在加鎖時分别對應
MarkWord
可能存儲的4種狀态。
- 輕量級鎖:00
- 重量級鎖:10
- GC标記(等待回收):11
- 偏向鎖:01
2.2、JDK1.6之後鎖更新(過程)
優化前與優化後說明
JDK1.6之前
也就是沒有優化之前,
synchronized
是重量級鎖(
悲觀鎖
:很悲觀認為每次拿對應資料都有其他線程會與其進行争搶,每次都會進行上鎖,其他線程若想使用指定資源就需要阻塞等待),每次都要進行線程挂起與喚醒,這樣會很浪費資源,影響性能,是以之後對
synchronized
關鍵字進行優化。
JDK1.6
之後,将鎖分為了無鎖、偏向鎖、輕量級鎖、重量級鎖這四種狀态,根據不同的情況來進行鎖更新,而不是從始到終都是重量級鎖。
四種狀态描述
鎖更新過程:
無鎖
->
偏向鎖
->
輕量級鎖
->
重量級鎖
,這幾個狀态會随着競争情況逐漸更新。
- 注意:鎖可以更新但是不可以降級。
無鎖
:也就是沒有對資源進行鎖定,所有的線程都能夠通路并修改同一個資源,同時隻有一個線程能夠修改成功,其他修改失敗線程會不斷重複直到修改成功。
偏向鎖
:指定的對象鎖一直被同一線程執行,不存在多個線程競争情況,該線程在後序執行中能夠自動擷取鎖,進而降低了頻繁擷取鎖帶來的性能開銷。
- 簡而言之:偏向鎖就是偏向第一個加鎖的線程,該線程是不會主動釋放鎖的,隻有當其他線程嘗試競争偏向鎖才會釋放。
- 撤銷操作:需要在某個時間點沒有位元組碼正在執行,先暫停擁有偏向鎖的線程,之後判斷鎖對象是否處于被鎖定狀态。
- 若是線程不處于活動狀态,會将對象頭設定成無鎖狀态,并撤銷偏向鎖。
- 若是線程處于活躍狀态,更新為輕量級鎖狀态。
輕量級鎖
:當鎖是偏向鎖時,該鎖被第二個線程B鎖通路,之後該對象的偏向鎖就會更新為輕量級鎖,第二個線程B會通過自旋形式不斷嘗試擷取鎖,線程不會進入阻塞狀态,提升了性能。
- 自旋等待說明:若是目前隻有一個等待線程,該線程會通過自旋進行等待(不斷嘗試擷取鎖),若是自旋超過一定數量時,輕量級鎖就會更新為重量級鎖。
- 更新鎖情況:若是目前一個線程已持有鎖,另一個線程在自旋嘗試擷取鎖,此時第三個線程來訪,輕量級鎖會更新為重量級鎖。
重量級鎖
:即當一個線程擷取鎖之後,其餘所有等待擷取該鎖的線程都會處于阻塞狀态。
- 通過對象内部螢幕(
)實作,其本質是依賴于底層作業系統的monitor
(互斥鎖)實作,作業系統實作線程之間的切換需要從使用者态切換到核心态,該切換成本就會很高。Mutex Lock
總結
synchronized
鎖更新實際上就是把原本的重量級鎖(悲觀鎖)變為了鎖更新的一個過程,
- 若是隻有1個線程擷取請求使用該鎖,該鎖為
(請求鎖時會自動擷取)。偏向鎖
- 接着若是有第二個線程來擷取通路,偏向鎖更新為
。第二個線程會進行自旋請求擷取該鎖,超過一定數量自旋時會更新為輕量級鎖
或第三個線程來訪(第一個線程持有鎖、第二個線程在自旋狀态)會更新為重量級鎖
。重量級鎖
- 重量級鎖即為若是某個線程占有了該鎖,那麼其他線程都會進入阻塞等待。
不同鎖使用情況:
- 偏向鎖适用于單線程的情況。
- 輕量級鎖适用于競争不激烈的情況。(與樂觀鎖使用範圍類似)
- 重量級鎖适用于競争激烈情況。
三、同步方法、代碼塊反編譯
包含JVM規範The Java® Virtual Machine Specification[2]中關于方法級同步、同步代碼塊說明
JVM規範網址:Synchronization
測試程式如下,包含一個同步方法以及一個同步代碼塊:
/**
* @ClassName Synchronized
* @Author ChangLu
* @Date 2021/4/7 13:23
* @Description synchronized(同步方法、代碼塊)反編譯測試
*/
public class SynchronizedTest {
public static void main(String[] args) {
}
//同步方法
public static synchronized void test01(){
System.out.println("test01()");
}
//同步代碼塊
public void test02(){
synchronized (this){
System.out.println("test02()");
}
}
}
通過JDK工具指令來進行反編譯(記得先編譯為class位元組碼檔案): javap -v SynchronizedTest.class
同步方法反編譯:
JVM
采用
ACC_synchronized
标記符來實作同步
根據
JVM
虛拟機對于同步方法的規範大緻内容如下:
- 方法級的同步是隐式的,同步方法的常量池中有一個
标志。ACC_SYNCHRONIZED
- 當某個線程要通路某個方法時,會檢查是否有
标志。ACC_SYNCHRONIZED
- 若是有:則需要先擷取螢幕鎖(
),接着開始執行方法,方法執行完之後再釋放鎖。這個時間段若是有其他線程來請求執行方法,會因為無法獲得螢幕而被阻斷掉。monitor
- 若是有:則需要先擷取螢幕鎖(
- 若是擷取鎖後在方法執行過程中,發生異常,并且方法内部并沒有處理該異常,那麼異常被抛到外面之前螢幕鎖會先進行釋放。
同步代碼塊反編譯:
JVM
采用
monitorenter
、
monitorexit
兩個指令來實作同步。
-
- 下面圖檔是官網上的注釋内容,
是傳入f
代碼塊中的對象 ,中間會有上鎖與解鎖的過程。synchronized
- 下面圖檔是官網上的注釋内容,
根據
JVM
虛拟機對于同步代碼塊的規範大緻内容如下:
- 将
指令認為是加鎖;monitorenter
認為是釋放鎖。monitorexit
- 每個對象維護着一個記錄着被鎖次數的計數器。對于未被鎖定的對象計數器為0。
- 當一個線程獲得鎖(
指令)後,計數器自增1;線程釋放鎖(moitorenter
指令),計數器自減1。對于同一個線程再次擷取該對象鎖時,計數器會再次自增,同了解鎖自減(可以說明moitorexit
是可重入鎖)。synchronized
- 當一個線程獲得鎖(
- 當計數器為0時,鎖就相當于被釋放了,那麼其他線程便可以去擷取該鎖。
參考文章
[1]. Java對象在記憶體中的布局
[2]. synchoronize原理
[3]. synchronized鎖的更新原理是什麼?以及各個鎖的狀态對比
我是長路,感謝你的耐心閱讀,如有問題請指出,我會聽取建議并進行修正。
歡迎關注我的公衆号:長路Java,其中會包含軟體安裝等其他一些資料,包含一些視訊教程以及學習路徑分享。
程式設計學習qq群:891507813 我們可以一起探讨學習
注明:轉載可,需要附帶上文章連結