點(diǎn)贊再看,養(yǎng)成習(xí)慣梯醒,公眾號(hào)搜一搜【一角錢技術(shù)】關(guān)注更多原創(chuàng)技術(shù)文章。本文 GitHub org_hejianhui/JavaStudy 已收錄颠区,有我的系列文章娜饵。
前言
- 并發(fā)編程從操作系統(tǒng)底層工作的整體認(rèn)識(shí)開(kāi)始
- 深入理解Java內(nèi)存模型(JMM)及volatile關(guān)鍵字
- 深入理解CPU緩存一致性協(xié)議(MESI)
在并發(fā)編程中存在線程安全問(wèn)題瞻惋,主要原因有:1.存在共享數(shù)據(jù) 2.多線程共同操作共享數(shù)據(jù)括袒。關(guān)鍵字synchronized可以保證在同一時(shí)刻次兆,只有一個(gè)線程可以執(zhí)行某個(gè)方法或某個(gè)代碼塊,同時(shí)synchronized可以保證一個(gè)線程的變化可見(jiàn)(可見(jiàn)性)锹锰,即可以代替volatile芥炭。
設(shè)計(jì)同步器的意義
多線程編程中漓库,有可能會(huì)出現(xiàn)多個(gè)線程同時(shí)訪問(wèn)同一個(gè)共享、可變資源的情況园蝠,這個(gè)資源我們稱之為 臨界資源 渺蒿。這種資源可能是:對(duì)象、變量砰琢、文件等蘸嘶。
- 共享:資源可以由多個(gè)線程同時(shí)訪問(wèn)良瞧;
- 可變:資源可以在其生命周期內(nèi)被修改陪汽。
引出的問(wèn)題:由于線程執(zhí)行的過(guò)程是不可控的,所以需要采用同步機(jī)制來(lái)協(xié)調(diào)對(duì)對(duì)象可變狀態(tài)的訪問(wèn)褥蚯。
如何解決線程并發(fā)安全問(wèn)題挚冤?
實(shí)際上,所有的并發(fā)模式在解決線程安全問(wèn)題時(shí)赞庶,采用的方案都是 序列化訪問(wèn)臨界資源 训挡。即在同一時(shí)刻,只能有一個(gè)線程訪問(wèn)臨界資源歧强,也稱作 同步互斥訪問(wèn) 澜薄。
Java 中,提供了兩種方式來(lái)實(shí)現(xiàn)同步互斥訪問(wèn):synchronized 和 Lock
同步器的本質(zhì)就是加鎖摊册。加鎖目的:序列化訪問(wèn)臨界資源肤京,即同一時(shí)刻只能有一個(gè)線程訪問(wèn)臨界資源(同步互斥訪問(wèn))
不過(guò)有一點(diǎn)需要區(qū)別的是:當(dāng)多個(gè)線程執(zhí)行一個(gè)方法時(shí),該方法內(nèi)部的局部變量并不是臨界資源茅特,因?yàn)檫@些局部變量是在每個(gè)線程的私有棧中忘分,引出不具有共享性,不會(huì)導(dǎo)致線程安全問(wèn)題白修。
synchronized 原理分析
synchronized 內(nèi)在鎖是一種對(duì)象鎖(鎖的是對(duì)象而非引用)妒峦,作用粒度是對(duì)象,可以用來(lái)實(shí)現(xiàn)對(duì)臨界資源的同步互斥訪問(wèn)兵睛,是可重入的肯骇。
加鎖的方式:
- 同步實(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());
}
}
- 同步類方法笛丙,鎖是當(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);
}
}
- 同步代碼塊若债,鎖是括號(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)勾拉,通過(guò)內(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)減少鎖操作的開(kā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)視器鎖),加鎖過(guò)程如下所示:
任何一個(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é)不一樣霞赫,但是都可以通過(guò)成對(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),其過(guò)程如下:
- 如果 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ā)生異常退出釋放鎖;
通過(guò)上面兩段描述伴挚,我們應(yīng)該能很清楚的看出 Synchronized 的實(shí)現(xiàn)原理靶衍,Synchronized 的語(yǔ)義底層是通過(guò)一個(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)看败徊,方法的同步并沒(méi)有通過(guò)指令 monitorenter 和 monitorexit 來(lái)完成(理論上其實(shí)也可以通過(guò)這兩條指令來(lái)實(shí)現(xiàn))帚呼,不過(guò)相對(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
訪問(wèn)標(biāo)識(shí)是否被設(shè)置眷蜈,如果設(shè)置了沪哺,執(zhí)行線程將先獲取 monitor,獲取成功之后才能執(zhí)行方法體酌儒,方法執(zhí)行完后再釋放 monitor辜妓。再方法執(zhí)行期間,其他任何線程都無(wú)法獲得同一個(gè) monitor 對(duì)象忌怎。
兩種同步方式本質(zhì)上沒(méi)有區(qū)別籍滴,只是方法的同步是一種隱式的方式來(lái)實(shí)現(xiàn),無(wú)需通過(guò)字節(jié)碼來(lái)完成榴啸。兩個(gè)指令的執(zhí)行是 JVM 通過(guò)調(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)就帶了一把看不見(jiàn)的鎖纤掸,它叫做內(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í)訪問(wèn)一段同步代碼時(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 鎖便是通過(guò)這種方式獲取鎖的,也是為什么 Java 中任意對(duì)象可以作為鎖的原因锯梁,同時(shí) notify/notifyAll/wait 等方法會(huì)使用到 Monitor 鎖對(duì)象即碗,所以必須在同步代碼塊中使用。監(jiān)視器 Monitor 有兩種同步方式:互斥與協(xié)作陌凳。多線程環(huán)境下線程之間如果需要共享數(shù)據(jù)剥懒,需要解決互斥訪問(wèn)數(shù)據(jù)的問(wèn)題,監(jiān)視器可以確保監(jiān)視器上的數(shù)據(jù)在同一時(shí)刻只會(huì)有一個(gè)線程在訪問(wèn)合敦。
那么有個(gè)問(wèn)題來(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ī)可以通過(guò) 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ī)(暫不考慮開(kāi)啟壓縮指針的場(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ī)可以通過(guò)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ì)開(kāi)啟指針壓縮,所以基本上也是按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)于開(kāi)啟了指針壓縮的32GB堆空間每篷。
這是為什么呢瓣戚?看下面引用中的紅字(來(lái)自openjdk wiki:https://wiki.openjdk.java.net/display/HotSpot/CompressedOops)。32bit最大尋址空間是4GB焦读,開(kāi)啟了壓縮指針之后呢子库,一個(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開(kāi)源工具包俗他,引入下方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í)過(guò)程
鎖的狀態(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)是開(kāi)啟偏向鎖和輕量級(jí)鎖的,可以通過(guò) -XX:-UseBiasedLocking
來(lái)禁用偏向鎖辞居。下圖為鎖的升級(jí)全過(guò)程:
偏向鎖
偏向鎖 是Java 6 之后加入的新鎖楷怒,它是一種針對(duì)加鎖操作的優(yōu)化手段,經(jīng)過(guò)研究發(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ò)程趴樱,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作馒闷,從而也就提供程序的性能。所以叁征,對(duì)于沒(méi)有競(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)開(kāi)啟偏向鎖
開(kāi)啟偏向鎖:-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í)間訪問(wèn)同一鎖的場(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)過(guò)若干次循環(huán)后纳猫,如果得到鎖,就順利進(jìn)入臨界區(qū)竹捉。如果還不能獲得鎖芜辕,那就會(huì)將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式活孩,這種方式確實(shí)也是可以提升效率的物遇。最后沒(méi)有辦法也就只能升級(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)貌似沒(méi)什么問(wèn)題窍株,且在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í)編譯),通過(guò)對(duì)運(yùn)行上下文的掃描苟耻,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖篇恒,通過(guò)這種方式消除沒(méi)有必要的鎖,可以節(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í)必須開(kāi)啟逃逸分析
-XX:+DoEscapeAnalysis 開(kāi)啟逃逸分析
-XX:+EliminateLoacks 表示開(kāi)啟鎖消除
鎖消除是發(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í)我們可以通過(guò)編譯器將其優(yōu)化糖埋,將鎖消除 宣吱,前提是 Java 必須運(yùn)行在 server 模式(server模式會(huì)比client模式作更多的優(yōu)化),同時(shí)必須開(kāi)啟逃逸分析:
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
逃逸分析:比如上面的代碼瞳别,它要看 sBuf 是否可能逃出它的作用域征候?如果將 sBuf 作為方法的返回值進(jìn)行返回,那么它在方法外部可能被當(dāng)作一個(gè)全局對(duì)象使用祟敛,就有可能發(fā)生線程安全問(wèn)題疤坝,這時(shí)就可以說(shuō) sBuf 這個(gè)對(duì)象發(fā)生逃逸了,因而不應(yīng)將 append 操作的鎖消除垒棋,但我們上面的代碼沒(méi)有發(fā)生鎖逃逸卒煞,鎖消除就可以帶來(lái)一定的性能提升。
逃逸分析
使用逃逸分析叼架,編譯器可以對(duì)代碼做如下優(yōu)化:
- 同步省略畔裕。如果一個(gè)對(duì)象被發(fā)現(xiàn)只能從一個(gè)線程被訪問(wèn)到,那么對(duì)于這個(gè)對(duì)象的操作可以不考慮同步乖订。
- 將堆分配轉(zhuǎn)化為棧分配扮饶。如果一個(gè)對(duì)象在子程序中被分配,要使指向該對(duì)象的指針永遠(yuǎn)不會(huì)逃逸乍构,對(duì)象可能是棧分配的候選甜无,而不是堆分配。
- 分離對(duì)象或標(biāo)量替換哥遮。有的對(duì)象可能不需要作為一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu)也可以被訪問(wèn)到岂丘,那么對(duì)象的部分(或全部)可以不存儲(chǔ)在內(nèi)存,而是存儲(chǔ)在CPU寄存器中眠饮。
是不是所有的對(duì)象和數(shù)組都會(huì)在堆內(nèi)存分配空間奥帘?答案是:不一定
在Java代碼運(yùn)行時(shí),通過(guò) JVM 參數(shù)可以指定釋放開(kāi)啟逃逸分析:
-XX:+DoEscapeAnalysis 表示開(kāi)啟逃逸分析
-XX:-DoEscapeAnalysis 表示關(guān)閉逃逸分析
從JDK1.7 開(kāi)始已經(jīng)默認(rèn)開(kāi)啟逃逸分析仪召,如需關(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
*
* 開(kāi)啟逃逸分析
* 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ū),有的一部分存在線程椪倌龋空間
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ì)象
第二種情況:開(kāi)啟逃逸分析
設(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
文章持續(xù)更新吐句,可以公眾號(hào)搜一搜「 一角錢技術(shù) 」第一時(shí)間閱讀胁后, 本文 GitHub org_hejianhui/JavaStudy 已經(jīng)收錄,歡迎 Star嗦枢。