并發(fā)編程之Synchronized深入理解
前言
- 并發(fā)編程從操作系統(tǒng)底層工作的整體認(rèn)識(shí)開始
- 深入理解Java內(nèi)存模型(JMM)及volatile關(guān)鍵字
- 深入理解CPU緩存一致性協(xié)議(MESI)
在并發(fā)編程中存在線程安全問題,主要原因有:1.存在共享數(shù)據(jù) 2.多線程共同操作共享數(shù)據(jù)。關(guān)鍵字synchronized可以保證在同一時(shí)刻,只有一個(gè)線程可以執(zhí)行某個(gè)方法或某個(gè)代碼塊,同時(shí)synchronized可以保證一個(gè)線程的變化可見(可見性),即可以代替volatile。
設(shè)計(jì)同步器的意義
多線程編程中,有可能會(huì)出現(xiàn)多個(gè)線程同時(shí)訪問同一個(gè)共享、可變資源的情況,這個(gè)資源我們稱之為 臨界資源 。這種資源可能是:對(duì)象、變量、文件等。
- 共享:資源可以由多個(gè)線程同時(shí)訪問;
- 可變:資源可以在其生命周期內(nèi)被修改。
引出的問題:由于線程執(zhí)行的過程是不可控的,所以需要采用同步機(jī)制來(lái)協(xié)調(diào)對(duì)對(duì)象可變狀態(tài)的訪問。
如何解決線程并發(fā)安全問題?
實(shí)際上,所有的并發(fā)模式在解決線程安全問題時(shí),采用的方案都是 序列化訪問臨界資源 。即在同一時(shí)刻,只能有一個(gè)線程訪問臨界資源,也稱作 同步互斥訪問 。
Java 中,提供了兩種方式來(lái)實(shí)現(xiàn)同步互斥訪問:synchronized 和 Lock
同步器的本質(zhì)就是加鎖。加鎖目的:序列化訪問臨界資源,即同一時(shí)刻只能有一個(gè)線程訪問臨界資源(同步互斥訪問)
不過有一點(diǎn)需要區(qū)別的是:當(dāng)多個(gè)線程執(zhí)行一個(gè)方法時(shí),該方法內(nèi)部的局部變量并不是臨界資源,因?yàn)檫@些局部變量是在每個(gè)線程的私有棧中,引出不具有共享性,不會(huì)導(dǎo)致線程安全問題。
synchronized 原理分析
synchronized 內(nèi)在鎖是一種對(duì)象鎖(鎖的是對(duì)象而非引用),作用粒度是對(duì)象,可以用來(lái)實(shí)現(xiàn)對(duì)臨界資源的同步互斥訪問,是可重入的。
加鎖的方式:
1.同步實(shí)例方法,鎖是當(dāng)前實(shí)例對(duì)象
synchronized修飾非靜態(tài)方法 鎖定的是該類的實(shí)例, 同一實(shí)例在多線程中調(diào)用才會(huì)觸發(fā)同步鎖定 所以多個(gè)被synchronized修飾的非靜態(tài)方法在同一實(shí)例下 只能多線程同時(shí)調(diào)用一個(gè)。
- public class Juc_LockOnThisObject {
- private Integer stock = 10;
- public synchronized void decrStock() {
- --stock;
- System.out.println(ClassLayout.parseInstance(this).toPrintable());
- }
- }
2.同步類方法,鎖是當(dāng)前類對(duì)象
synchronized修飾靜態(tài)方法 鎖定的是類本身,而不是實(shí)例, 同一個(gè)類中的所有被synchronized修飾的靜態(tài)方法, 只能多線程同時(shí)調(diào)用一個(gè)。
- public class Juc_LockOnClass {
- private static int stock;
- public static synchronized void decrStock(){
- System.out.println(--stock);
- }
- }
3.同步代碼塊,鎖是括號(hào)里面的對(duì)象
synchronized塊 直接鎖定指定的對(duì)象,該對(duì)象在多個(gè)地方的同步鎖定塊,只能多線程同時(shí)執(zhí)行其中一個(gè)。
- public class Juc_LockOnObject {
- public static Object object = new Object();
- private Integer stock = 10;
- public void decrStock() {
- //T1,T2
- synchronized (object) {
- --stock;
- if (stock <= 0) {
- System.out.println("庫(kù)存售罄");
- return;
- }
- }
- }
- }
synchronized底層原理
synchronized 是基于 JVM內(nèi)置鎖 實(shí)現(xiàn),通過內(nèi)部對(duì)象Monitor(監(jiān)視器鎖) 實(shí)現(xiàn),基于進(jìn)入與退出 Monitor 對(duì)象實(shí)現(xiàn)方法與代碼塊同步,監(jiān)視器鎖的實(shí)現(xiàn)依賴底層操作系統(tǒng)的 Mutex lock (互斥鎖)實(shí)現(xiàn),它是一個(gè)重量級(jí)鎖性能較低。當(dāng)然,JVM 內(nèi)置鎖在1.5之后版本做了大量的優(yōu)化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Eliminaction)、輕量級(jí)鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應(yīng)性自旋(Adaptive Spinning)等技術(shù)來(lái)減少鎖操作的開銷,內(nèi)置鎖的并發(fā)性能已經(jīng)基本與 Lock 持平。
synchronized 關(guān)鍵字被編譯成字節(jié)碼后會(huì)被翻譯成 monitorenter 和 monitorexit 兩條指令分別在同步塊邏輯代碼的起始位置與結(jié)束位置。

示例:
- public class Juc_LockOnObject {
- public static Object object = new Object();
- private Integer stock = 10;
- public void decrStock() {
- //T1,T2
- synchronized (object) {
- --stock;
- if (stock <= 0) {
- System.out.println("庫(kù)存售罄");
- return;
- }
- }
- }
- }
反編譯結(jié)果如下:

Monitor 監(jiān)視器鎖
每個(gè)同步對(duì)象都有一個(gè)自己的 Monitor(監(jiān)視器鎖),加鎖過程如下所示:

任何一個(gè)對(duì)象都有一個(gè) Monitor 與之關(guān)聯(lián),當(dāng)且一個(gè) Monitor 被持有后,它將處于鎖定狀態(tài)。
Synchronized 在JVM里面的實(shí)現(xiàn)都是 基于進(jìn)入和退出 **Monitor 對(duì)象 **來(lái)實(shí)現(xiàn)方法同步和代碼塊同步,雖然具體實(shí)現(xiàn)細(xì)節(jié)不一樣,但是都可以通過成對(duì)的 MonitorEnter 和 MonitorExit 指令來(lái)實(shí)現(xiàn)。
- monitorenter :每個(gè)對(duì)象都是一個(gè)監(jiān)視器鎖(monitor)。當(dāng) monitor 被占用時(shí)就會(huì)處于鎖定狀態(tài),線程執(zhí)行 monitorenter 指令時(shí)嘗試獲取 monitor 的所有權(quán),其過程如下:如果 monitor 的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者;如果線程已經(jīng)占有該 monitor,只是重新進(jìn)入,則進(jìn)入 monitor 的進(jìn)入數(shù)加1;如果其他線程已經(jīng)占有 monitor,則該線程進(jìn)入阻塞狀態(tài),直到 monitor 的進(jìn)入數(shù)為0,再重新嘗試獲取 monitor 的所有權(quán)。
- monitorexit :執(zhí)行 monitorexit 的線程必須是 objectref 所對(duì)應(yīng)的 monitor 的所有者。指令執(zhí)行時(shí),monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那么線程退出 monitor,不再是這個(gè) monitor 的所有者。其他被這個(gè) monitor 阻塞的線程可以嘗試去獲取這個(gè) monitor 的所有權(quán)。
- monitorexit,指令出現(xiàn)了兩次,第1次為同步正常退出釋放鎖;第2次為發(fā)生異常退出釋放鎖;
通過上面兩段描述,我們應(yīng)該能很清楚地看出 Synchronized 的實(shí)現(xiàn)原理,Synchronized 的語(yǔ)義底層是通過一個(gè) monitor 的對(duì)象來(lái)完成,其實(shí) wait/notify 等方法也依賴于 monitor 對(duì)象,這就是為什么只有在同步的塊或者方法中才能調(diào)用 wait/nofity 等方法,否則會(huì)拋出 java.lang.IllegalMonitorStateException 的異常原因。
示例:看一個(gè)同步方法
- package com.niuh;
- public class SynchronizedMethod {
- public synchronized void method() {
- System.out.println("Hello Word!");
- }
- }
反編譯結(jié)果:

從編譯的結(jié)果來(lái)看,方法的同步并沒有通過指令 monitorenter 和 monitorexit 來(lái)完成(理論上其實(shí)也可以通過這兩條指令來(lái)實(shí)現(xiàn)),不過相對(duì)于普通方法,其常量池多了 ACC_SYNCHRONIZED 標(biāo)識(shí)符。
JVM 就是根據(jù)該標(biāo)識(shí)符來(lái)實(shí)現(xiàn)方法的同步的:當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)識(shí)是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取 monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放 monitor。在方法執(zhí)行期間,其他任何線程都無(wú)法獲得同一個(gè) monitor 對(duì)象。
兩種同步方式本質(zhì)上沒有區(qū)別,只是方法的同步是一種隱式的方式來(lái)實(shí)現(xiàn),無(wú)需通過字節(jié)碼來(lái)完成。兩個(gè)指令的執(zhí)行是 JVM 通過調(diào)用操作系統(tǒng)的互斥原語(yǔ)mutex 來(lái)實(shí)現(xiàn),被阻塞的線程會(huì)被掛起,等待重新調(diào)度,會(huì)導(dǎo)致“用戶態(tài)和內(nèi)核態(tài)”兩個(gè)態(tài)直接來(lái)回切換,對(duì)性能有較大的影響。
什么是 Monitor ?
可以把它理解為 一個(gè)同步工具,也可以描述為 一種同步機(jī)制,它通常被描述為一個(gè)對(duì)象。與一切皆對(duì)象一樣,所有的Java對(duì)象是天生的 Monitor,每一個(gè)Java 對(duì)象都有成為 Monitor 的潛質(zhì),因?yàn)樵贘ava的設(shè)計(jì)中,每一個(gè)Java對(duì)象自打娘胎里出來(lái)就帶了一把看不見的鎖,它叫做內(nèi)部鎖或者 Monitor鎖。也就是通常說(shuō)的 Synchronized 的對(duì)象鎖,MarkWord 鎖標(biāo)識(shí)位為10,其中指針指向的是 Monitor 對(duì)象的起始地址。在 Java 虛擬機(jī)(HotSpot)中,Monitor 是由 ObjectMonitor 實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于 HotSpot 虛擬機(jī)源碼 ObjectMonitor.hpp 文件,C++實(shí)現(xiàn)的):
- ObjectMonitor() {
- _header = NULL;
- _count = 0; // 記錄個(gè)數(shù)
- _waiters = 0,
- _recursions = 0;
- _object = NULL;
- _owner = NULL;
- _WaitSet = NULL; // 處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet
- _WaitSetLock = 0 ;
- _Responsible = NULL ;
- _succ = NULL ;
- _cxq = NULL ;
- FreeNext = NULL ;
- _EntryList = NULL ; // 處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表
- _SpinFreq = 0 ;
- _SpinClock = 0 ;
- OwnerIsThread = 0 ;
- }
ObjectMonitor 中有兩個(gè)隊(duì)列,_WaitSet 和 _EntryList,用來(lái)保存 ObjectWaiter 對(duì)象列表(每個(gè)等待鎖的線程都會(huì)被封裝成 ObjectWaiter 對(duì)象),_owner 指向持有 ObjectWaiter 對(duì)象的線程,當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(shí):
- 首先會(huì)先進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對(duì)象的 monitor 后,進(jìn)入 _owner 區(qū)域并把 monitor 中的 _owner 變量設(shè)置為當(dāng)前線程,同時(shí) monitor 中的計(jì)數(shù)器 count 加1;
- 若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的 monitor,owner 變量恢復(fù)為 null,count 自減1,同時(shí)該線程進(jìn)入 _WaitSet 集合中等待被喚醒;
- 若當(dāng)前線程執(zhí)行完畢,也架構(gòu)釋放 monitor(鎖) 并復(fù)位 count 的值,以便其他線程進(jìn)入獲取 monitor(鎖);
同時(shí),Monitor 對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭 Mark Word 中(存儲(chǔ)的指針的指向),Synchronized 鎖便是通過這種方式獲取鎖的,也是為什么 Java 中任意對(duì)象可以作為鎖的原因,同時(shí) notify/notifyAll/wait 等方法會(huì)使用到 Monitor 鎖對(duì)象,所以必須在同步代碼塊中使用。監(jiān)視器 Monitor 有兩種同步方式:互斥與協(xié)作。多線程環(huán)境下線程之間如果需要共享數(shù)據(jù),需要解決互斥訪問數(shù)據(jù)的問題,監(jiān)視器可以確保監(jiān)視器上的數(shù)據(jù)在同一時(shí)刻只會(huì)有一個(gè)線程在訪問。
那么有個(gè)問題來(lái)了,我們知道 synchronized 加鎖加在對(duì)象上,對(duì)象是如何記錄鎖狀態(tài)的呢?答案是鎖狀態(tài)被記錄在每個(gè)對(duì)象的對(duì)象頭(Mark Word)中,下面一起來(lái)認(rèn)識(shí)下對(duì)象的內(nèi)存布局。
對(duì)象的內(nèi)存布局
HostSpot 虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為三塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)、對(duì)齊填充(Padding)。
- 對(duì)象頭:比如 hash 碼,對(duì)象所屬的年代,對(duì)象鎖,鎖狀態(tài)標(biāo)識(shí),偏向鎖(線程)ID,偏向時(shí)間,數(shù)組長(zhǎng)度(數(shù)組對(duì)象)等。Java 對(duì)象頭一般占用2個(gè)機(jī)器碼(在32位虛擬機(jī)中,1個(gè)機(jī)器碼等于4字節(jié),也就是32bit,在64位虛擬機(jī)中,1個(gè)機(jī)器碼是8個(gè)字節(jié),也就是64big),但是 如果對(duì)象是數(shù)組類型,則需要3個(gè)機(jī)器碼,因?yàn)?JVM 虛擬機(jī)可以通過 Java 對(duì)象的元數(shù)據(jù)信息確定Java 對(duì)象的大小,但是無(wú)法從數(shù)組的元數(shù)據(jù)來(lái)確認(rèn)數(shù)組的大小,所以用一塊記錄數(shù)組長(zhǎng)度。
- 實(shí)例數(shù)據(jù):存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息;
- 對(duì)齊填充:由于虛擬機(jī)要求 對(duì)象起始位置必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對(duì)齊;

對(duì)象頭
HotSpot 虛擬機(jī)的 對(duì)象頭 包括兩部分信息
第一部分是 “Mark Word”,用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)識(shí)、線程持有的鎖、偏向鎖ID、偏向時(shí)間戳等等,它是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵。
這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(暫不考慮開啟壓縮指針的場(chǎng)景)中分別為32個(gè)和64個(gè)Bits,官方稱它為“Mark Word”。對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)的數(shù)據(jù)很多,其實(shí)已經(jīng)超出了 32、64 位Bitmap 結(jié)構(gòu)所能記錄的限度,但是對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,考慮到虛擬機(jī)的空間效率, Mark Word 被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的信息,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用到自己的存儲(chǔ)空間。
例如在32位的HotSpot虛擬機(jī)中對(duì)象未被鎖定的狀態(tài)下,Mark Word 的32位Bits空間中的:
- 25Bits用于存儲(chǔ)對(duì)象哈希碼(HashCode)
- 4Bits用于存儲(chǔ)對(duì)象分代年齡
- 2Bits用于存儲(chǔ)鎖標(biāo)識(shí)位,
- 1Bits固定為0
- 其他狀態(tài)(輕量級(jí)鎖定、重量級(jí)鎖定、GC標(biāo)記、可偏向)下的對(duì)象存儲(chǔ)內(nèi)容如下表所示
但是如果對(duì)象是數(shù)組類型,則需要三個(gè)機(jī)器碼,因?yàn)镴VM虛擬機(jī)可以通過Java 對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小,但是無(wú)法從數(shù)組的元數(shù)據(jù)來(lái)確認(rèn)數(shù)組的大小,所以用一塊記錄數(shù)組的長(zhǎng)度。
對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,但是考慮到虛擬機(jī)的空間效率,Mark Word 被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù),它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,也就是說(shuō),Mark Word會(huì)隨著程序的運(yùn)行發(fā)生變化。變化狀態(tài)如下:
32位虛擬機(jī)

64位虛擬機(jī)
現(xiàn)在我們虛擬機(jī)基本是64位的,而64位的對(duì)象頭有點(diǎn)浪費(fèi)空間,JVM默認(rèn)會(huì)開啟指針壓縮,所以基本上也是按32位的形式記錄對(duì)象頭的。
- 手動(dòng)設(shè)置-XX:+UseCompressedOops
哪些信息會(huì)被壓縮?
- 對(duì)象的全局靜態(tài)變量(即類屬性)
- 對(duì)象頭信息:64位平臺(tái)下,原生對(duì)象頭大小為16字節(jié),壓縮后為12字節(jié)
- 對(duì)象的引用類型:64位平臺(tái)下,引用類型本身大小為8字節(jié),壓縮后為4字節(jié)
- 對(duì)象數(shù)組類型:64位平臺(tái)下,數(shù)組類型本身大小為24字節(jié),壓縮后16字節(jié)
在Scott oaks寫的《java性能權(quán)威指南》第八章8.22節(jié)提到了當(dāng)heap size堆內(nèi)存大于32GB是用不了壓縮指針的,對(duì)象引用會(huì)額外占用20%左右的堆空間,也就意味著要38GB的內(nèi)存才相當(dāng)于開啟了指針壓縮的32GB堆空間。
這是為什么呢?看下面引用中的紅字(來(lái)自openjdk wiki:https://wiki.openjdk.java.net/display/HotSpot/CompressedOops)。32bit最大尋址空間是4GB,開啟了壓縮指針之后呢,一個(gè)地址尋址不再是1byte,而是8byte,因?yàn)椴还苁?2bit的機(jī)器還是64bit的機(jī)器,java對(duì)象都是8byte對(duì)齊的,而類是java中的基本單位,對(duì)應(yīng)的堆內(nèi)存中都是一個(gè)一個(gè)的對(duì)象。
Compressed oops represent managed pointers (in many but not all places in the JVM) as 32-bit values which must be scaled by a factor of 8 and added to a 64-bit base address to find the object they refer to. This allows applications to address up to four billion objects (not bytes), or a heap size of up to about 32Gb. At the same time, data structure compactness is competitive with ILP32 mode.
對(duì)象頭分析工具
運(yùn)行時(shí)對(duì)象頭鎖狀態(tài)分析工具JOL,他是OpenJDK開源工具包,引入下方maven依賴
- <dependency>
- <groupId>org.openjdk.jol</groupId>
- <artifactId>jol-core</artifactId>
- <version>0.10</version>
- </dependency>
打印markword
- System.out.println(ClassLayout.parseInstance(object).toPrintable());
- //object為我們的鎖對(duì)象
鎖的膨脹升級(jí)過程
鎖的狀態(tài)總共有四種,無(wú)鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)到重量級(jí)鎖,但是鎖的升級(jí)是單向的,也就是說(shuō)只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí)。從JDK1.6中默認(rèn)是開啟偏向鎖和輕量級(jí)鎖的,可以通過 -XX:-UseBiasedLocking 來(lái)禁用偏向鎖。下圖為鎖的升級(jí)全過程:

偏向鎖
偏向鎖 是Java 6 之后加入的新鎖,它是一種針對(duì)加鎖操作的優(yōu)化手段,經(jīng)過研究發(fā)現(xiàn),在大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,因此為來(lái)減少同一線程獲取鎖(會(huì)涉及到一些 CAS 操作,耗時(shí))的代價(jià)而引入偏向鎖。偏向鎖的核心思想是,如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí),無(wú)需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作,從而也就提供程序的性能。所以,對(duì)于沒有競(jìng)爭(zhēng)的場(chǎng)合,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個(gè)線程申請(qǐng)相同的鎖。但是對(duì)于鎖競(jìng)爭(zhēng)畢竟激烈的場(chǎng)合,偏向鎖就失效了,因?yàn)檫@樣的場(chǎng)合極有可能每次申請(qǐng)鎖的線程都是不同的,因此這種場(chǎng)合下不應(yīng)該使用偏向鎖,否則會(huì)得不償失,需要注意的是,偏向鎖失敗后,并不會(huì)立即膨脹為重量級(jí)鎖,而是先升級(jí)為輕量級(jí)鎖。
- 默認(rèn)開啟偏向鎖
- 開啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 關(guān)閉偏向鎖:-XX:-UseBiasedLocking
輕量級(jí)鎖
倘若偏向鎖失敗,虛擬機(jī)并不會(huì)立即升級(jí)為重量級(jí)鎖,它還會(huì)嘗試使用一種稱為輕量級(jí)鎖的優(yōu)化手段(1.6之后加入的),此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級(jí)鎖的結(jié)構(gòu)。輕量級(jí)鎖能夠提升程序性能的依據(jù)是:“對(duì)絕大部分的鎖,在整個(gè)同步周期內(nèi)都不存在競(jìng)爭(zhēng)”,注意這是經(jīng)驗(yàn)數(shù)據(jù)。需要了解的是,輕量級(jí)鎖所適應(yīng)的場(chǎng)景是線程交替執(zhí)行同步塊的場(chǎng)合,如果存在同一時(shí)間訪問同一鎖的場(chǎng)合,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。
自旋鎖
輕量級(jí)鎖失敗后,虛擬機(jī)為了避免線程真實(shí)地操作系統(tǒng)層面掛起,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下,線程持有鎖的時(shí)間都不會(huì)太長(zhǎng),如果直接掛起操作系統(tǒng)層面的線程可能會(huì)得不償失,畢竟操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高,因此自旋鎖會(huì)假設(shè)在不久將來(lái),當(dāng)前的線程可以獲得鎖,因此虛擬機(jī)會(huì)讓當(dāng)前想要獲取鎖的線程做幾個(gè)空循環(huán)(這也是稱為自旋的原因),一般不會(huì)太久,可能是50個(gè)循環(huán)或100個(gè)循環(huán),在經(jīng)過若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會(huì)將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實(shí)也是可以提升效率的。最后沒有辦法也就只能升級(jí)為重量級(jí)鎖了。
鎖粗化
通常情況下,為了保證多線程間的有效并發(fā),會(huì)要求每個(gè)線程持有鎖的時(shí)間盡可能短,但是在某些情況下,一個(gè)程序?qū)ν粋€(gè)鎖不間斷、高頻地請(qǐng)求、同步與釋放,會(huì)消耗掉一定的系統(tǒng)資源,因?yàn)殒i的講求、同步與釋放本身會(huì)帶來(lái)性能損耗,這樣高頻的鎖請(qǐng)求就反而不利于系統(tǒng)性能的優(yōu)化了,雖然單次同步操作的時(shí)間可能很短。鎖粗化就是告訴我們?nèi)魏问虑槎加袀€(gè)度,有些情況下我們反而希望把很多次鎖的請(qǐng)求合并成一個(gè)請(qǐng)求,以降低短時(shí)間內(nèi)大量鎖請(qǐng)求、同步、釋放帶來(lái)的性能損耗。
一種極端的情況如下:
- public void doSomethingMethod(){
- synchronized(lock){
- //do some thing
- }
- //這是還有一些代碼,做其它不需要同步的工作,但能很快執(zhí)行完畢
- synchronized(lock){
- //do other thing
- }
- }
上面的代碼是有兩塊需要同步操作的,但在這兩塊需要同步操作的代碼之間,需要做一些其它的工作,而這些工作只會(huì)花費(fèi)很少的時(shí)間,那么我們就可以把這些工作代碼放入鎖內(nèi),將兩個(gè)同步代碼塊合并成一個(gè),以降低多次鎖請(qǐng)求、同步、釋放帶來(lái)的系統(tǒng)性能消耗,合并后的代碼如下:
- public void doSomethingMethod(){
- //進(jìn)行鎖粗化:整合成一次鎖請(qǐng)求、同步、釋放
- synchronized(lock){
- //do some thing
- //做其它不需要同步但能很快執(zhí)行完的工作
- //do other thing
- }
- }
注意:這樣做是有前提的,就是中間不需要同步的代碼能夠很快速地完成,如果不需要同步的代碼需要花很長(zhǎng)時(shí)間,就會(huì)導(dǎo)致同步塊的執(zhí)行需要花費(fèi)很長(zhǎng)的時(shí)間,這樣做也就不合理了。
另一種需要鎖粗化的極端的情況是:
- for(int i=0;i<size;i++){
- synchronized(lock){
- }
- }
上面代碼每次循環(huán)都會(huì)進(jìn)行鎖的請(qǐng)求、同步與釋放,看起來(lái)貌似沒什么問題,且在jdk內(nèi)部會(huì)對(duì)這類代碼鎖的請(qǐng)求做一些優(yōu)化,但是還不如把加鎖代碼寫在循環(huán)體的外面,這樣一次鎖的請(qǐng)求就可以達(dá)到我們的要求,除非有特殊的需要:循環(huán)需要花很長(zhǎng)時(shí)間,但其它線程等不起,要給它們執(zhí)行的機(jī)會(huì)。
鎖粗化后的代碼如下:
- synchronized(lock){
- for(int i=0;i<size;i++){
- }
- }
鎖消除
鎖消除是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機(jī)在JIT編譯時(shí)(可以簡(jiǎn)單立即為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱為即時(shí)編譯),通過對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無(wú)意義的請(qǐng)求鎖時(shí)間,如下 StringBuffer 的 append 是一個(gè)同步方法,但是在 add 方法中的 StringBuffer 屬于一個(gè)局部變量,并且不會(huì)被其他線程所使用,因此 StringBuffer 不可能在共享資源競(jìng)爭(zhēng)的情景,JVM 會(huì)自動(dòng)將其鎖消除。鎖消除的依據(jù)是逃逸分析的數(shù)據(jù)支持。
鎖消除,前提是java必須運(yùn)行在 server模式(server模式會(huì)比client模式做更多的優(yōu)化),同時(shí)必須開啟逃逸分析
- -XX:+DoEscapeAnalysis 開啟逃逸分析
- -XX:+EliminateLoacks 表示開啟鎖消除
鎖消除是發(fā)生在編譯器級(jí)別的一種鎖優(yōu)化方式,有時(shí)候我們寫的代碼完全不需要加鎖,卻執(zhí)行了加鎖操作。比如,StringBuffer 類的 append 操作
- @Override
- public synchronized StringBuffer append(String str) {
- toStringCache = null;
- super.append(str);
- return this;
- }
從源碼中可以看出,append 方法使用了 synchronized 關(guān)鍵字,它是線程安全的。但我們可能僅在線程內(nèi)部把 StringBuffer 當(dāng)作局部變量使用:
- package com.niuh;
- public class Juc_LockAppend {
- public static void main(String[] args) {
- long start = System.currentTimeMillis();
- int size = 10000;
- for (int i = 0; i < size; i++) {
- createStringBuffer("一角錢技術(shù)", "為分享技術(shù)而生");
- }
- long timeCost = System.currentTimeMillis() - start;
- System.out.println("createStringBuffer:" + timeCost + " ms");
- }
- public static String createStringBuffer(String str1, String str2) {
- StringBuffer sBuf = new StringBuffer();
- sBuf.append(str1);// append方法是同步操作
- sBuf.append(str2);
- return sBuf.toString();
- }
- }
代碼中 createStringBuffer 方法中的局部對(duì)象 sBuf ,就只在該方法內(nèi)的作用域有效,不同線程同時(shí)調(diào)用 createStringBuffer() 方法時(shí),都會(huì)創(chuàng)建不同的 sBuf 對(duì)象,因此此時(shí)的 append 操作若是使用同步操作,就是白白浪費(fèi)系統(tǒng)資源。
這時(shí)我們可以通過編譯器將其優(yōu)化,將鎖消除 ,前提是 Java 必須運(yùn)行在 server 模式(server模式會(huì)比client模式作更多的優(yōu)化),同時(shí)必須開啟逃逸分析:
- -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
逃逸分析:比如上面的代碼,它要看 sBuf 是否可能逃出它的作用域?如果將 sBuf 作為方法的返回值進(jìn)行返回,那么它在方法外部可能被當(dāng)作一個(gè)全局對(duì)象使用,就有可能發(fā)生線程安全問題,這時(shí)就可以說(shuō) sBuf 這個(gè)對(duì)象發(fā)生逃逸了,因而不應(yīng)將 append 操作的鎖消除,但我們上面的代碼沒有發(fā)生鎖逃逸,鎖消除就可以帶來(lái)一定的性能提升。
逃逸分析
使用逃逸分析,編譯器可以對(duì)代碼做如下優(yōu)化:
- 同步省略。如果一個(gè)對(duì)象被發(fā)現(xiàn)只能從一個(gè)線程被訪問到,那么對(duì)于這個(gè)對(duì)象的操作可以不考慮同步。
- 將堆分配轉(zhuǎn)化為棧分配。如果一個(gè)對(duì)象在子程序中被分配,要使指向該對(duì)象的指針永遠(yuǎn)不會(huì)逃逸,對(duì)象可能是棧分配的候選,而不是堆分配。
- 分離對(duì)象或標(biāo)量替換。有的對(duì)象可能不需要作為一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu)也可以被訪問到,那么對(duì)象的部分(或全部)可以不存儲(chǔ)在內(nèi)存,而是存儲(chǔ)在CPU寄存器中。
是不是所有的對(duì)象和數(shù)組都會(huì)在堆內(nèi)存分配空間?答案是:不一定
在Java代碼運(yùn)行時(shí),通過 JVM 參數(shù)可以指定釋放開啟逃逸分析:
- -XX:+DoEscapeAnalysis 表示開啟逃逸分析
- -XX:-DoEscapeAnalysis 表示關(guān)閉逃逸分析
從JDK1.7 開始已經(jīng)默認(rèn)開啟逃逸分析,如需關(guān)閉,需要指定 -XX:-DoEscapeAnalysis
逃逸分析案例:循環(huán)創(chuàng)建50W次 NiuhStudent 對(duì)象
- package com.niuh;
- public class T0_ObjectStackAlloc {
- /**
- * 進(jìn)行兩種測(cè)試
- * 關(guān)閉逃逸分析,同時(shí)調(diào)大堆空間,避免堆內(nèi)GC的發(fā)生,如果有GC信息將會(huì)被打印出來(lái)
- * VM運(yùn)行參數(shù):-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
- *
- * 開啟逃逸分析
- * VM運(yùn)行參數(shù):-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
- *
- * 執(zhí)行main方法后
- * jps 查看進(jìn)程
- * jmap -histo 進(jìn)程ID
- */
- public static void main(String[] args) {
- long start = System.currentTimeMillis();
- for (int i = 0; i < 500000; i++) {
- alloc();
- }
- long end = System.currentTimeMillis();
- //查看執(zhí)行時(shí)間
- System.out.println("cost-time " + (end - start) + " ms");
- try {
- Thread.sleep(100000);
- } catch (InterruptedException e1) {
- e1.printStackTrace();
- }
- }
- private static NiuhStudent alloc() {
- //Jit對(duì)編譯時(shí)會(huì)對(duì)代碼進(jìn)行 逃逸分析
- //并不是所有對(duì)象存放在堆區(qū),有的一部分存在線程??臻g
- NiuhStudent student = new NiuhStudent();
- return student;
- }
- static class NiuhStudent {
- private String name;
- private int age;
- }
- }
第一種情況:關(guān)閉逃逸分析,同時(shí)調(diào)大堆空間,避免堆內(nèi)GC的發(fā)生,如果有GC信息將會(huì)被打印出來(lái)
- 設(shè)置VM運(yùn)行參數(shù):-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
如下所示創(chuàng)建了50W個(gè)實(shí)例對(duì)象
第二種情況:開啟逃逸分析
- 設(shè)置VM運(yùn)行參數(shù):-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
如下未創(chuàng)建50W個(gè)實(shí)例對(duì)象

PS:以上代碼提交在 Github :
https://github.com/Niuh-Study/niuh-juc-final.git