文章已同步發(fā)表于微信公眾號JasonGaoH拖云,深入解析volatile關鍵字
volatile關鍵字
和synchronized關鍵字
一樣,在Java多線程開發(fā)中踏施,是一道必須要跨越的檻石蔗。之前有篇文章已經(jīng)分析過synchronized關鍵字
的原理,synchronized關鍵字的原理畅形,這一次养距,我們來一步一步分析下volatile關鍵字
的工作原理。
本文篇幅稍微有點長日熬,希望您能耐心看下去棍厌,并有所收獲。
volatile關鍵字的使用
首先竖席,我們從一個簡單的程序來入手耘纱。
public class VolatileFoo {
//init_value的最大值
final static int MAX = 5;
//init_value的初始值
static int init_value = 0;
public static void main(String[] args) {
//啟動一個Reader線程,當發(fā)現(xiàn)local_value和init_value不同時毕荐,
//則輸出init_value被修改的信息
new Thread(() -> {
int localValue = init_value;
while(localValue < MAX) {
if(init_value != localValue) {
System.out.println("this init_value is updated to " + init_value);
//對local_value重新賦值
localValue = init_value;
}
}
},"Readder").start();
new Thread(() -> {
int localValue = init_value;
while(localValue < MAX) {
System.out.println("this init_value will be changed to " + ++localValue);
//對local_value重新賦值
init_value = localValue;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Updater").start();
}
}
上面的程序分別啟動了兩個線程束析,一個線程負責對變量進行修改,一個線程負責對變量進行輸出憎亚。
運行程序员寇,輸出結果如下:
this init_value will be changed to 1
this init_value is updated to 1
this init_value will be changed to 2
this init_value will be changed to 3
this init_value will be changed to 4
this init_value will be changed to 5
從輸出信息我們發(fā)現(xiàn),Reader線程沒有感知到init_value的變化第美,我們期望的是在Updater進程更新init_value的值之后蝶锋,Reader進程能夠打印出變化的init_value的值,但結果并不是我們期望的那樣斋日。
我們嘗試在init_value前面加上volatile
牲览。
static volatile int init_value = 0;
接著我們再運行下這個程序,輸出結果如下:
this init_value will be changed to 1
this init_value is updated to 1
this init_value will be changed to 2
this init_value is updated to 2
this init_value will be changed to 3
this init_value is updated to 3
this init_value will be changed to 4
this init_value is updated to 4
this init_value will be changed to 5
this init_value is updated to 5
這個時候Reader線程就能夠感受到init_value的值的變化了恶守,并且在條件不滿足時程序就退出了運行第献。
那么為什么加了個volatile就正常了呢, volatile關鍵字的作用到底是什么呢兔港?
想要徹底搞清楚volatile關鍵字
庸毫,還需要具備Java內(nèi)存模型、CPU緩存模型衫樊、匯編指令等相關知識的飒赃,接下來,我們接下來一步一步來拆解問題科侈。
CPU緩存模型
要想對volatile有比較深刻的理解载佳,首先我們需要對CPU的緩存模型有一定的認識。
在計算機中臀栈,所有的運算操作都是由CPU的寄存器來完成的蔫慧,CPU指令的執(zhí)行過程需要涉及數(shù)據(jù)的讀取和寫入操作,CPU所能訪問的所有數(shù)據(jù)只能是計算機的主存(通常是指RAM)权薯,雖然CPU的發(fā)展頻率不斷得到提升姑躲,但受制于制造工藝以及成本的限制,計算機的內(nèi)存反倒在訪問速度上沒有多大的突破盟蚣,因此CPU的處理速度和內(nèi)存的訪問速度之間的差距越拉越大黍析,通常這種差距可以達到上千倍,極端情況下甚至會在上萬倍以上屎开。
由于兩邊速度嚴重的不對等阐枣,通過傳統(tǒng)FSB直連內(nèi)存的訪問方式會導致CPU資源受到極大的限制,降低CPU整體的吞吐量奄抽,于是就有了CPU和主內(nèi)存直接增加緩存的設計蔼两,現(xiàn)在緩存數(shù)量都可以增加到3級了,最靠近CPU的緩存為L1,然后依次是L2,L3和主內(nèi)存如孝,CPU緩存模型圖如下所示:
Cache的出現(xiàn)是為了解決CPU直接訪問內(nèi)存效率低下的問題宪哩,程序在運行的過程中,會將運算所需要的數(shù)據(jù)從主內(nèi)存復制一份到CPU Cache中第晰,這樣CPU計算時就可以直接對CPU Cache中的數(shù)據(jù)進行讀取和寫入锁孟,當運算結束之后,再將CPU Cache中最新的數(shù)據(jù)刷新到主內(nèi)存當中茁瘦,CPU通過直接訪問Cache的方式提到直接訪問主內(nèi)存的方式極大地提高了CPU的吞吐能力品抽,有個CPU Cache之后,整體的CPU和主內(nèi)存之間的交互的架構大致如下圖所示:
Java內(nèi)存模型
由于緩存的出現(xiàn)甜熔,極大地提高了CPU的吞吐能力圆恤,但是同時也引入了緩存不一致的問題。在多處理器系統(tǒng)中腔稀,每個處理器都有自己的的高速緩存盆昙,而它們又共享同一主內(nèi)存羽历,當多個處理器的運算任務都設計到同一塊內(nèi)存區(qū)域時,將可能導致各自的緩存數(shù)據(jù)不一致淡喜,這個時候就需要通過緩存一致性協(xié)議
來保證數(shù)據(jù)的正確性秕磷,不同的操作系統(tǒng)使用緩存一致性協(xié)議都各不相同。
因為各種硬件和操作系統(tǒng)的內(nèi)存訪問是有差異的炼团,Java為了程序能在各種平臺下運行達到一致的內(nèi)存訪問效果澎嚣,于是定義了Java內(nèi)存模型(Java Memory Mode,JMM)來對特定內(nèi)存或高速緩存的讀寫訪問過程進行抽象瘟芝。
Java內(nèi)存模型定義了線程和主內(nèi)存之間的抽象關系易桃,具體如下。
- 共享變量存儲于主內(nèi)存之中锌俱,每個線程都可以訪問晤郑。
- 每個線程都有私有的工作內(nèi)存和本地內(nèi)存。
- 工作內(nèi)存值存儲該線程對共享變量的副本嚼鹉。
- 線程不能直接操作主內(nèi)存贩汉,只有先操作了工作內(nèi)存之后才能寫入主內(nèi)存。
- 工作內(nèi)存和Java內(nèi)存模型一樣也是一個抽象的概念锚赤,它其實并不存在匹舞,它涵蓋了緩存、寄存器线脚、編譯優(yōu)化以及硬件等赐稽。
Java內(nèi)存模型
定義了一套主內(nèi)存和工作內(nèi)存的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存浑侥、如何從工作內(nèi)存同步到主內(nèi)存之類的實現(xiàn)細節(jié)姊舵。具體有8種操作來完成,分別為lock寓落、unlock括丁、read、load伶选、use史飞、assign、store和write仰税。除此之外构资,Java內(nèi)存模型
還規(guī)定在執(zhí)行這8種操作的時候必須滿足8種規(guī)則,由于篇幅問題陨簇,這里就不一一列舉了吐绵,具體可參看深入理解Java虛擬機第12章的Java內(nèi)存模型與線程。
Java內(nèi)存模型
是一個抽象的概念,其與計算機硬件的結構并不完全一樣己单,比如計算機物理內(nèi)存不會存在棧內(nèi)存和堆內(nèi)存的劃分唉窃,無論是堆內(nèi)存還是虛擬機棧內(nèi)存都會對應到物理的主內(nèi)存,當然也有一部分堆棧內(nèi)存數(shù)據(jù)可能會存入CPU Cache寄存器中荷鼠。具體可參考下圖:
對于volatile變量的特殊規(guī)則
介紹了CPU緩存模型
以及Java內(nèi)存模型
之后句携,我們再來說volatile關鍵字
榔幸,這樣更能加深我們對于volatile關鍵字
的理解允乐。
volatile關鍵字
是Java虛擬機提供的最輕量級的同步機制,很多人由于對它理解不夠削咆,往往更愿意使用synchronized來做同步牍疏。
Java內(nèi)存模型
對volatile關鍵字
定義了一些特殊的訪問規(guī)則,當一個變量被volatile修飾后拨齐,它將具備兩種特性鳞陨,或者說volatile具有下列兩層語義:
- 第一、保證了不同線程對這個變量進行讀取時的可見性瞻惋, 即一個線程修改了某個變量的值厦滤, 這新值對其他線程來說是立即可見的。 (volatile 解決了線程間共享變量的可見性問題)歼狼。
- 第二掏导、禁止進行指令重排序, 阻止編譯器對代碼的優(yōu)化羽峰。
針對第一點趟咆,volatile
保證了不同線程對這個變量進行讀取時的可見性,具體表現(xiàn)為:
- 第一: 使用 volatile 關鍵字會強制將在某個線程中修改的共享變量的值立即寫入主內(nèi)存梅屉。
- 第二: 使用 volatile 關鍵字的話值纱, 當線程 2 進行修改時, 會導致線程 1 的工作內(nèi)存中變量的緩存行無效(反映到硬件層的話坯汤, 就是 CPU 的 L1或者 L2 緩存中對應的緩存行無效);
- 第三: 由于線程 1 的工作內(nèi)存中變量的緩存行無效虐唠, 所以線程 1再次讀取變量的值時會去主存讀取。
基于這一點惰聂,所以我們經(jīng)常會看到文章中或者書本中會說volatile 能夠保證可見性疆偿。
volatile 能夠保證可見性,但是volatile不能保證程序的原子性庶近。
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race ++;
}
private static final int THREAD_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREAD_COUNT];
for(int i =0 ;i<THREAD_COUNT;i++) {
threads[i] = new Thread(() ->{
for(int j =0;j< 10000;j++) {
increase();
}
});
threads[i].start();
}
while(Thread.activeCount() > 1)
Thread.yield();
System.out.println(race);
}
}
這段代碼發(fā)起了20個線程翁脆,每個線程對race變量進行10000次自增操作,如果這段代碼能夠正確并發(fā)的話鼻种,最后輸出的結果應該是200000反番。我們運行完這段代碼之后,并沒有獲得期望的結果,而且發(fā)現(xiàn)每次運行程序罢缸。輸出的結果都不一樣篙贸,都是一個小于200000的數(shù)字。
問題就出在自增運算”race++“之中枫疆,我們用javap反編譯這段代碼后發(fā)現(xiàn)只有一行代碼的increase()方法在Class文件中是由4條字節(jié)碼指令構成的爵川。
public static void increase();
Code:
0: getstatic #13 // Field race:I
3: iconst_1
4: iadd
5: putstatic #13 // Field race:I
8: return
從字節(jié)碼層面上很容易分析出原因了:當getstatic指令把race的值取到操作棧時,volatile關鍵字保證了race的值此時是正確的息楔,但是在執(zhí)行iconst_1寝贡、iAdd這些指令的時候,其他線程可能已經(jīng)把race的值加大了值依,而在操作棧訂的值就變成了過期的數(shù)據(jù)圃泡,所以putstati指令執(zhí)行后就可能把較小的值同步回主內(nèi)存中去了。
其實這里我們通過字節(jié)碼來分析這個問題是不嚴謹?shù)脑赶眨驗榧词咕幾g出來的只有一條字節(jié)指令颇蜡,也并不意味執(zhí)行這條指令就是一個原子操作。一條字節(jié)碼指令在解釋執(zhí)行時辆亏,解釋器將要運行許多行代碼才能實現(xiàn)它的語義风秤,如果是編譯執(zhí)行,一條字節(jié)碼指令也可能轉化成若干條本地機器碼指令扮叨。關于解釋執(zhí)行和編譯執(zhí)行缤弦,我們還會再講到。
由于volatile變量只能保證可見性甫匹,在不符合以下兩條規(guī)則的運算場景中甸鸟,我們?nèi)匀灰ㄟ^加鎖(synchronized或java.util.concurrent中的原子類)來保證原子性。
- 運輸結果并不依賴變量的當前值兵迅,或者能夠確保只有單一的線程修改變量的值抢韭。
- 變量不需要與其他狀態(tài)變量共同參與不變約束。
類似下面的場景就時候采用volatile來控制并發(fā)恍箭。
volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while(!shutdownRequested) {
//do stuff
}
}
如果我們想讓上面的那個自增操作保持原子性刻恭,我們可以使用AtomicInteger,具體程序如下扯夭,這里就不多做介紹了鳍贾。
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileTest {
// public static volatile int race = 0;
public static AtomicInteger race =new AtomicInteger(0);
public static void increase() {
// race ++;
race.incrementAndGet();
}
private static final int THREAD_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREAD_COUNT];
for(int i =0 ;i<THREAD_COUNT;i++) {
threads[i] = new Thread(() ->{
for(int j =0;j< 10000;j++) {
increase();
}
});
threads[i].start();
}
while(Thread.activeCount() > 1)
Thread.yield();
System.out.println(race.get());
}
}
回到volatile關鍵字
的第二層語義:禁止指令重排。
普通的變量僅僅會保證在該方法的執(zhí)行過程中所有依賴賦值結果的地方都能獲取到正確的結果交洗,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致骑科。
我們用一段偽代碼來幫助下理解:
Map configOptions;
char[] configText;
//此變量必須定義為volatile
volatile boolean initialized = false;
//假設一下代碼在線程A中執(zhí)行
//模擬讀取配置信息,當讀取完成后將initialized設置為true以通知其他線程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processCongigOptions(configText,configOptions);
initialized = true
//假設一下代碼在線程B中執(zhí)行
//等待initialized為true构拳,代表線程A已經(jīng)吧配置信息初始化完成
while(!initialized) {
sleep();
}
//使用線程A中初始化好的配置信息
doSomethingWithConfig();
上面這段代碼如果定義的initialized沒有使用volatile來修飾咆爽,就可能會由于指令重排序的優(yōu)化梁棠,導致位于線程A中最后一句代碼initialized = true
被提前執(zhí)行(這里雖然使用Java作為偽代碼,但所指的重排序優(yōu)化是機器級的優(yōu)化操作斗埂,提前執(zhí)行時值這句話對于的匯編代碼被提前執(zhí)行)符糊,這樣在線程B中使用配置信息的代碼就可能出現(xiàn)錯誤,而volatile能避免此類情況的發(fā)生呛凶。
volatile關鍵字深入解析
上面講到volatile關鍵字的兩層語義
男娄,那么volatile保證可見性以及有序性到底是如何做到的呢?它的底層邏輯是什么呢漾稀?
這里我們嘗試獲得Java程序的匯編代碼模闲,通過比較變量加入volatile修飾和未加入volatile修飾的區(qū)別。
這里主要使用的是HSDIS插件县好,HSDIS是一個Sun官方推薦的HotSpot虛擬機JIT編譯代碼的反匯編插件围橡,網(wǎng)上有關于這個插件的下載,不過有的鏈接已經(jīng)失效缕贡,我這里是從這里獲取的,hsdis拣播,再把這個clone下來之后晾咪,編譯成功之后,使用下面這個命令拷貝到jre的server目錄,具體可以查看這個repo中README文件贮配,里面寫的很詳細谍倦。
sudo cp build/macosx-amd64/hsdis-amd64.dylib /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/server/
接下來就可以嘗試反匯編了。
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if(instance ==null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}
上面這個是我們嘗試反匯編的程序代碼泪勒,如果是命令行昼蛀,我們可以使用下面這個指令。
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Singleton
如果是eclipse圆存,在下圖的VM arguments中添加XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
,然后運行程序叼旋,這樣在控制臺就會輸出匯編代碼。
程序運行后沦辙,在控制臺會輸出很多內(nèi)容,由于輸出太大夫植,所以截取了前面一段輸出。
Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
Loaded disassembler from /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/lib/server/hsdis-amd64.dylib
Decoding compiled method 0x0000000112e9ad50:
Code:
[Disassembling for mach='i386:x86-64']
[Entry Point]
[Constants]
# {method} {0x000000010ce1f000} 'hashCode' '()I' in 'java/lang/String'
# [sp+0x40] (sp of caller)
0x0000000112e9aec0: mov 0x8(%rsi),%r10d
0x0000000112e9aec4: shl $0x3,%r10
0x0000000112e9aec8: cmp %rax,%r10
0x0000000112e9aecb: jne 0x0000000112de0e60 ; {runtime_call}
0x0000000112e9aed1: data16 data16 nopw 0x0(%rax,%rax,1)
0x0000000112e9aedc: data16 data16 xchg %ax,%ax
[Verified Entry Point]
0x0000000112e9aee0: mov %eax,-0x14000(%rsp)
.....
得到這個輸出之后油讯,我使用Singleton
全局搜索了下详民,發(fā)現(xiàn)還無結果。
反編譯的卻沒有得到相應的內(nèi)容陌兑,這是什么問題呢沈跨?
帶著這個問題Google了好久,終于搞明白原因了兔综。
于是我們又要來補充些虛擬機編譯的知識了饿凛。
我們在使用
java -version
查看JDK版本的時候隅俘,可以看到最后有個mixed mode
,這里其實表明的是Java 虛擬機的編譯方式笤喳,在HotSpot虛擬機中为居,提供了兩種編譯模式:解釋執(zhí)行 和 即時編譯(JIT,Just-In-Time)杀狡,即時編譯也可以稱為編譯執(zhí)行蒙畴,解釋執(zhí)行即逐條翻譯字節(jié)碼為可運行的機器碼,而即時編譯則以方法為單位將字節(jié)碼翻譯成機器碼呜象。
我們在反編譯Singleton這個類的時候膳凝,因為虛擬機使用的是解釋執(zhí)行,這樣我們是得不到匯編代碼的恭陡。在深入理解Java虛擬機一書中介紹可以加上-Xcomp
來觸發(fā)JIT編譯蹬音,但是我用的是JDK1.8,這個 -Xcomp`已經(jīng)被移除了休玩,具體哪個版本被移除了著淆,目前我也沒仔細研究過了。
那要怎樣才能觸發(fā)JIT編譯呢拴疤?答案是循環(huán)永部。通過足夠多次數(shù)的循環(huán)來觸發(fā)JIT編譯。我們需要確保寫的Java方法被調用的次數(shù)足夠多呐矾,以觸發(fā)C1(客戶端)編譯苔埋,并大約10000次觸發(fā)C2(服務器)編譯器并打開高級優(yōu)化。換句話說蜒犯,要想查看匯編代碼组橄,我們所寫的Java源代碼文件不能太過于簡單,要足夠復雜罚随。
注意:C1玉工,C2都是HotSpot虛擬機內(nèi)置的即時編譯器。C1:即Client編譯器毫炉,面向對啟動性能有要求的客戶端GUI程序瓮栗,采用的優(yōu)化手段比較簡單,因此編譯的時間較短瞄勾。C2:即Server編譯器费奸,面向對性能峰值有要求的服務端程序,采用的優(yōu)化手段復雜进陡,因此編譯時間長愿阐,但是在運行過程中性能更好。
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if(instance ==null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
for(int i=0;i<100;i++) {
print();
}
}
private static void print() {
for(int i =0;i<=1000;i++) {
Singleton.getInstance();
}
}
}
于是我在代碼里加上了兩層循環(huán)趾疚,然后在嘗試獲取一些匯編代碼缨历。
這次發(fā)現(xiàn)終于能得到Singleton相關的匯編代碼了以蕴。
于是我們分別編譯了兩次,第一個是沒有使用volatile關鍵字修飾instance辛孵,第二個是使用volatile關鍵字丛肮,然后我們分別取出Singleton::getInstance
這一段來進行比較。
// 未使用volatile修飾
0x000000010d29e931: movabs $0x7955f12a8,%rsi ; {oop(a 'java/lang/Class' = 'main/Singleton')}
0x000000010d29e93b: mov %rax,%r10
0x000000010d29e93e: shr $0x3,%r10
0x000000010d29e942: mov %r10d,0x68(%rsi)
0x000000010d29e946: shr $0x9,%rsi
0x000000010d29e94a: movabs $0xfe403000,%rax
0x000000010d29e954: movb $0x0,(%rsi,%rax,1) ;*putstatic instance
; - main.Singleton::getInstance@24 (line 10)
// 使用volatile修飾
0x000000011435394f: movabs $0x7955f12a8,%rsi ; {oop(a 'java/lang/Class' = 'main/Singleton')}
0x0000000114353959: mov %rax,%r10
0x000000011435395c: shr $0x3,%r10
0x0000000114353960: mov %r10d,0x68(%rsi)
0x0000000114353964: shr $0x9,%rsi
0x0000000114353968: movabs $0x10db6e000,%rax
0x0000000114353972: movb $0x0,(%rsi,%rax,1)
0x0000000114353976: lock addl $0x0,(%rsp) ;*putstatic instance
; - main.Singleton::getInstance@24 (line 10)
雖然對于匯編指令了解不多魄缚,但還是能從兩個對比中看出差異所在宝与。
很明顯,在movb $0x0,(%rsi,%rax,1)
之后冶匹,加了volatile修飾的匯編代碼后面多了一條匯編指令lock addl $0x0,(%rsp)
习劫,這個操作相當于一個內(nèi)存屏障,指令重排時不能把后面的指令重排序到內(nèi)存屏障之前的位置嚼隘,當只有一個CPU訪問內(nèi)存時诽里,并不需要內(nèi)存屏障,當如果有兩個或多個CPU訪問同一塊內(nèi)存飞蛹,且其中有一個在觀測另一個谤狡,就需要內(nèi)存屏障來保證一致性了。lock addl $0x0,(%rsp)
表示把rsp的寄存器的值加0桩皿,這顯然是一個空操作豌汇,關鍵在于lock前綴。
查詢IA32手冊泄隔,lock前綴會強制執(zhí)行原子操作,它的作用是是的本CPU的Cache寫入了內(nèi)存宛徊,該寫入動作會引起別的CPU無效化其Cache佛嬉。所有通過這樣一個空操作,可讓前面volatile變量的便是對其他CPU可見闸天。
那為什么說它能禁止指令重排呢暖呕?從硬件架構上講,指令重排序是指CPU采用了運行將多條指令不按程序規(guī)定的順序分開發(fā)送給各相應的點了單元處理苞氮,但并不是指令任意重排湾揽,CPU需要能正確處理指令依賴情況以保障程序能得出正確的執(zhí)行結果。lock addl $0x0,(%rsp)
指令把修改同步到內(nèi)存時笼吟,意味著所有之前的操作都已經(jīng)執(zhí)行完成库物,這樣便形成了" 指令重排序無法越過內(nèi)存屏障"的效果。
總結來說贷帮,內(nèi)存屏障有兩個作用:
先于這個內(nèi)存屏障的指令必須先執(zhí)行戚揭, 后于這個內(nèi)存屏障的指令必須后執(zhí)行。
如果你的字段是volatile撵枢,在讀指令前插入讀屏障民晒,可以讓高速緩存中的數(shù)據(jù)失效精居,重新從主內(nèi)存加載數(shù)據(jù)。在寫指令之后插入寫屏障潜必,能讓寫入緩存的最新數(shù)據(jù)寫回到主內(nèi)存靴姿。
關于volatile關鍵字的介紹就到這里了,感謝磁滚,如果覺得還可以請幫忙點個贊链方,有問題歡迎留言討論柔吼。
參考
[深入理解Java虛擬機]
[Java高并發(fā)編程詳解]