如果你對java的volatile有著疑慮請閱讀本文,如果你想對volatile想有一個更深的了解,請閱讀本文.本文主要講的是volatile的寫happen-before在volatile讀之前所涉及相關(guān)的原理,以及在Hotspot中相關(guān)代碼的實現(xiàn).
首先從一段代碼開始講起,如下
初始化
[plain] view plain copyPRint? int a = 0, int b = 0; void foo(void) { a= 1; b= 1; } void bar(void) { while (b == 0) continue; If(a == 1) { System.out.println(“true”); }; }以上的代碼,threadA運行foo方法,threadB運行bar方法;在threadB中,能否在while循環(huán)跳出之后,即b=1的情況下,得到a一定等于1呢?答案是否定的.因為存在著編譯器的亂序和cpu指令的亂序,程序沒有按照我們所寫的順序執(zhí)行.
2.1編譯器亂序
編譯器是如何亂序的?編譯器在不改變單線程語義的前提之下,為了提高程序的運行速度,可以對指令進行亂序.
編譯器按照一個原則,進行亂序:不能改變單線程程序的行為.
在不改變單線程運行的結(jié)果,以上foo函數(shù)就有可能有兩種編譯結(jié)果:
第一種:
a = 1;
b= 1;
第二種:
b = 1;
a = 1;
如上,如果cpu按照第二種編譯結(jié)果執(zhí)行,那么就不能正確的輸出”true”.雖然亂序了,但是并沒有改變單線程threadA執(zhí)行foo的語義,所以上面的重排是允許的.
2.2.cpu亂序
2.2.1 cpu的結(jié)構(gòu)與cpu亂序:
首先來了解一下x86 cpu的結(jié)構(gòu):
c1,c2,c3 .. cn是160個用于整數(shù)和144個用于浮點的寄存器單元,用于存儲本地變量和函數(shù)參數(shù).cpu訪問寄存器的需要1cycle< 1ns,cpu訪問寄存器是最快的.
LoadBuffer,storeBuffer合稱排序緩沖(Memoryordering Buffers (MOB)),Load緩沖有64長度,store緩沖有36長度.buffer與L1進行數(shù)據(jù)傳輸,cpu無須等待.
L1是本地核心內(nèi)的緩存,被分成獨立的32K的數(shù)據(jù)緩存和32k指令緩存.訪問L1需要3cycles ~1ns.
L2緩存是本地核心內(nèi)的緩存,被設(shè)計為L1緩存與共享的L3緩存之間的緩沖,L2緩存大小為256K.訪問L2需要12cycles-3ns.
L3:在同插槽的所有核心共享L3緩存,L3緩存被分為數(shù)個2M的段,訪問L3需要38cycles~12ns.
DRAM:就是我們通常說的內(nèi)存,訪問內(nèi)存一般需要65ns.
cpu加載不同緩存級別的數(shù)據(jù),從上往下,需要的時間是越來越多的,而等待對cpu來說是極大的浪費;對于不同的插槽的cpu所擁有的一級二級緩存是不共享的,而不同的緩存之間需要保證一致性,主要通過MESI協(xié)議,為了保證不同緩存的一致性也是需要付出時間的,因為不同狀態(tài)之間的轉(zhuǎn)換需要等待消息的回復(fù),這個等待過程往往是阻塞的.
根據(jù)MESI協(xié)議,變量要從S狀態(tài)變?yōu)镸狀態(tài),本cpu要執(zhí)行read andmodify ,需要發(fā)出Invalidate消息,然后等待,待收到所有其它cpu的回復(fù)之后才執(zhí)行更新狀態(tài),其他操作也是如此,都有會等待的過程。這些過程都是阻塞的動作,無疑會給cpu的性能帶來巨大的損耗。
經(jīng)過不斷地改進,在寄存器與cache之間加上loadbuffer、storebuffer,來減小這些阻塞的時間。cpu要讀取數(shù)據(jù)時,先把讀請求發(fā)到loadbuffer中,無需等待其他cpu的響應(yīng),就可以先進行下一步操作。直到其他cpu的響應(yīng)結(jié)果達(dá)到之后,再處理這個讀請求的結(jié)果。cpu寫數(shù)據(jù)時,就把數(shù)據(jù)寫到storebuffer中,此時cpu認(rèn)為已經(jīng)把數(shù)據(jù)寫出去了,待到某個適合的時間點(cpu空閑時間),在把storebuffer的數(shù)據(jù)刷到主存中去。
根據(jù)以上的描述,可知在storebuffer中的數(shù)據(jù)有臨時可見性問題。即在storebuffer中的數(shù)據(jù)對本cpu是可見的(本cpu讀取數(shù)據(jù)時,可以直接從storebuffer中進行讀取),其它槽的cpu對storebuffer中的數(shù)據(jù)不可見。loadbuffer中的請求無法取到其他cpu修改的最新數(shù)據(jù),因為最新數(shù)據(jù)在其他cpu的storebuffer中。同時,在loadbuffer、storebuffer中的"請求"的完成都是異步的,即表現(xiàn)為它們完成的順序是不確定的,指令的亂序。
2.2.2 cpu亂序
cpu亂序的一種情況:
Processor 0
Processor 1
mov [ _x], 1
mov [ _y], 1
mov r1, [ _y]
mov r2, [_x]
Initially x == y == 0
解釋一下:x,y表示內(nèi)存中的變量,r1,r2表示寄存器, mov [_x],1,表示把1賦值給x變量,從順序執(zhí)行的角度來說,結(jié)果肯定是r1=1,或r2 =1;然而r1=0并且r2=0是存在的.
cpu實際執(zhí)行的情況可能如下
Processor 0
Processor 1
mov r1, [ _y] // (1)
mov r2, [_x] // (2)
mov [ _x], 1 // (3)
mov [ _y], 1 // (4)
如上(1),(2),(3),(4)的順序執(zhí)行,便會得到r1=0,r2=0.可以這么理解,大部分的處理器,在保證單線程執(zhí)行語義正確的基礎(chǔ)上,會根據(jù)一定的規(guī)則對指令進行重排.
為什么會出現(xiàn)上面的情況?
因為cpu會將寫放入storebuffer中,然后立刻返回不會等待,會將讀放入load buffer中,然后立刻返回,這兩個隊列都是異步的,他們寫讀的是不同地址,不存在依賴,兩者誰先完成是不知道的,如果讀指令先完成了,就出現(xiàn)上面的重排的情況.
3. 編譯器屏障與內(nèi)存屏障
由于存在著編譯器對程序指令的重排,這個需要編譯器屏障來保證程序的正確執(zhí)行,cpu也會對指令進行重排,這個需要內(nèi)存屏障來保證.所以屏障需要對編譯器和內(nèi)存同時起作用,來保證程序可以正確執(zhí)行.
因為我們大部分的軟件主要都是在x86_linux上運行的,所以我們主要研究的是x86的相關(guān)編譯器屏障和內(nèi)存屏障.
3.1 編譯屏障
Open jdk9中x86_linux的柵欄指令編寫如下:
static inline void compiler_barrier() {
__asm__ volatile ("" : : : "memory"); /*編譯器屏障*/
}
此處 __asm__ volatile("" : : : "memory"); 是內(nèi)嵌匯編.
解釋:
__asm__ :代表匯編代碼開始.
__volatile__:禁止編譯器對代碼進行某些優(yōu)化.
memory: memory代表是內(nèi)存;這邊用”memory”,來通知編譯器內(nèi)存的內(nèi)容已經(jīng)發(fā)生了修改,要重新生成加載指令(不可以從緩存寄存器中取).因為存在著內(nèi)存的改變,不可以將前后的代碼進行亂序.
asm volatile("" :::"memory"),這句內(nèi)嵌匯編作為編譯器屏障,可以防止編譯器對相鄰指令進行亂序,但是它無法阻止CPU的亂序;也就是說它僅僅禁止了編譯器的亂序優(yōu)化,不會阻止CPU的亂序執(zhí)行。
以下利用asm volatile("" ::: "memory")來驗證,對于屏障之后的讀,都是讀取的主存,而不是從寄存器中讀取的.當(dāng)然如果您不懂匯編和c++這部分您可以跳過.
實驗一:
[java] view plain copyprint?
#include <stdio.h> int foo = 10; int bar = 15; int main(void) { int ss = foo + 1; __asm__ __volatile__("":::"memory"); int foo1 = foo + 2; printf("ss=%d/n", ss); printf("foo1=%d/n", foo1); return0; } ![]()
#include <stdio.h>int foo = 10;int bar = 15;int main(void){ int ss = foo + 1; __asm__ __volatile__("":::"memory"); int foo1 = foo + 2; printf("ss=%d/n", ss); printf("foo1=%d/n", foo1); return0;}編譯命令:
g++ -S -O2 test1.cpp
編譯結(jié)果的部分匯編:
[plain] view plain copyprint?
main: .LFB30: .cfi_startproc movl foo(%rmain:.LFB30: .cfi_startproc movl foo(%rip), %eax //將foo變量從內(nèi)存加載到寄存器eax中 pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 leal 1(%rax), %edx //將rax變量加1賦值給edx寄存器也是ss變量. //rax是64位寄存器,它的低32位是eax寄存器. movl foo(%rip), %eax//將foo變量從內(nèi)存加載到寄存器eax中 movl $.LC0, %esi movl $1, %edi leal 2(%rax), %ebx//將rax變量加2賦值給edx寄存器也是foo1變量. xorl %eax, %eax call __printf_chk movl %ebx, %edx movl $.LC1, %esi movl $1, %edi xorl %eax, %eax call __printf_chk xorl %eax, %eax popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc實驗二:
[cpp] view plain copyprint?
#include <stdio.h> int foo = 10; int bar = 15; int main(void) { int ss = foo + 1; int foo1 = foo + 2; printf("ss=%d/n", ss); printf("foo1=%d/n", foo1); return0; } ![]()
#include <stdio.h>int foo = 10;int bar = 15;int main(void){ int ss = foo + 1; int foo1 = foo + 2; printf("ss=%d/n", ss); printf("foo1=%d/n", foo1); return0;}編譯命令:
g++ -S -O2 test2.cpp
編譯結(jié)果的部分匯編:
[plain] view plain copyprint?
main: FB30: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movl foo(%rip), %ebx //將foo變量從內(nèi)存加載到寄存器ebx中 movl $.LC0, %esi movl $1, %edi xorl %eax, %eax leal 1(%rbx), %edx//將rbx寄存器加1賦值給edx寄存器也是ss變量. //rbx是64位寄存器,它的低32位是ebx寄存器. call __printf_chk leal 2(%rbx), %edx//直接取rbx寄存器,加2賦值給foo1變量 movl $.LC1, %esi movl $1, %edi xorl %eax, %eax call __printf_chk xorl %eax, %eax popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc ![]()
main:.LFB30: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movl foo(%rip), %ebx //將foo變量從內(nèi)存加載到寄存器ebx中 movl $.LC0, %esi movl $1, %edi xorl %eax, %eax leal 1(%rbx), %edx//將rbx寄存器加1賦值給edx寄存器也是ss變量. //rbx是64位寄存器,它的低32位是ebx寄存器. call __printf_chk leal 2(%rbx), %edx//直接取rbx寄存器,加2賦值給foo1變量 movl $.LC1, %esi movl $1, %edi xorl %eax, %eax call __printf_chk xorl %eax, %eax popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc
結(jié)論:第一實驗加入了內(nèi)存屏障中,兩次分別從內(nèi)存中加載foo變量到寄存器中,然后進行操作,第二個實驗中取出了內(nèi)存屏障,第一次將foo變量從內(nèi)存中加載到了寄存器中,進行第一次操作,然后第二次并沒有從內(nèi)存中加載,而是直接去寄存器中的值去操作,由此可以看出屏障的后面是不可以緩存變量在寄存器中的,而是屏障后面的變量是需要重新從主存中加載的.
3.2 Java的內(nèi)存屏障
從上文可知:一個load操作需要進入loadbuffer中的,然后在從內(nèi)存中去加載,一個store操作需要進入storebuffer然后在寫入內(nèi)存.而兩個buffer之間是異步,導(dǎo)致出現(xiàn)了不同的亂序(重排),java定義了一系列的內(nèi)存屏障來指定指令的執(zhí)行順序.
Java中的內(nèi)存中的內(nèi)存屏障:
LoadLoad 屏障
StoreStore屏障
LoadStore 屏障
StoreLoad屏障
序列
Load1,Loadload,Load2
Store1,StoreStore,Store2
Load1,LoadStore,Store
Store1,StoreLoad,Load
作用
保證Load1所要讀入的數(shù)據(jù)能夠在被Load2和后續(xù)的load指令訪問前讀入。
保證Store1的數(shù)據(jù)在Store2以及后續(xù)Store指令操作相關(guān)數(shù)據(jù)之前對其它處理器可見
確保Load1的數(shù)據(jù)在Store2和后續(xù)Store指令被刷新之前讀取。
確保Store1的數(shù)據(jù)在被Load2和后續(xù)的Load指令讀取之前對其他處理器可見。
對buffer的影響
在Load buffer插入屏障,清空屏障之前的Load操作,然后才能執(zhí)行屏障之后的Load操作.
在Store buffer插入屏障,清空屏障之前的Store操作,然后才能執(zhí)行屏障之后的store操作.
在Load buffer插入屏障,清空屏障之前的Load操作,然后才能執(zhí)行屏障之后的Store操作.
在Load buffer, Store buffer中都插入屏障,必須清空屏障之前的Load操作并且清空屏障之前的store操作,然后才能執(zhí)行屏障之后的Load操作,或store操作.
StoreLoad屏障有可以同時獲得其它三種屏障(loadload,loadstore,storestore)的的效果,但是StoreLoad是一個比較耗性能的屏障.因為StoreLoad屏障在Loadbuffer, Store buffer中都插入屏障,必須清空屏障之前的Load操作并且清空屏障之前的store操作,然后才能執(zhí)行屏障之后的Load操作,或store操作.這使得之后的讀指令不能從store buffer中直接獲取,只能從緩存中獲取,綜合起來說,storeload屏障最耗性能。
3.2 Release 和Acquire,fence
Java中又定義了release和acquire,fence三種不同的語境的內(nèi)存柵欄.
如上圖,loadLoad和loadStore兩種柵欄對應(yīng)的都是acquire語境,,acquire語境一般定義在java的讀之前;在編譯器階段和cpu執(zhí)行的時候,acquire之后的所有的(讀和寫)操作不能越過acquire,重排到acquire之前,acquire指令之后所有的讀都是具有可見性的.
如上圖,StoreStore和LoadStore對應(yīng)的是release語境,release語境一般定義在java的寫之后,在編譯器和cpu執(zhí)行的時候,所有release之前的所有的(讀和寫)操作都不能越過release,重排到release之后,release指令之前所有的寫都會刷新到主存中去,其他核的cpu可以看到刷新的最新值.
對于fence,是由storeload柵欄組成的,比較消耗性能.在編譯器階段和cpu執(zhí)行時候,保證fence之前的任何操作不能重排到屏障之后,fence之后的任何操作不能重排到屏障之前.fence具有acquire和release這兩個都有的語境,即可以將fence之前的寫刷新到內(nèi)存中,fence之后的讀都是具有可見性的.
3.3 x86中的內(nèi)存柵欄與實現(xiàn)
內(nèi)存屏障,也稱內(nèi)存柵欄,內(nèi)存柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對內(nèi)存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點之后的操作。語義上,內(nèi)存屏障之前的所有寫操作都要寫入主存;內(nèi)存屏障之后的讀操作,直接讀取的主存,可以獲得內(nèi)存屏障之前的寫操作的結(jié)果。
完全內(nèi)存屏障(full memory barrier)保障了早于屏障的內(nèi)存讀寫操作的結(jié)果提交到內(nèi)存之后,再執(zhí)行晚于屏障的讀寫操作,在loadbuffer和storebuffer中插入屏障,清空屏障之前的讀和寫操作。X86中對應(yīng)MFence;
內(nèi)存讀屏障(read memory barrier)僅確保了內(nèi)存讀操作.在loadbuffe中插入屏障,清空屏障之前的讀操作;LFence
內(nèi)存寫屏障(write memory barrier)僅保證了內(nèi)存寫操作.在storebuffer中插入屏障,清空屏障之的寫操作; SFence
接下來我們看看在源碼中如何實現(xiàn),主要看open jdk1.9,為什么,jdk1.8里面對柵欄的實現(xiàn)千奇百怪,作者能力有限,不能全部了解jvm開發(fā)者當(dāng)時為啥那么實現(xiàn),而jdk1.9的實現(xiàn)相對比比較清晰.
在jdk1.9中對storestore,storeload,loadload,loadstore的實現(xiàn)如下:
[plain] view plain copyprint?
static inline void compiler_barrier() { __asm__ volatile ("" : : : "memory"); } inline void Orderaccess
::loadload() { compiler_barrier(); } inline void OrderAccess::storestore() {compiler_barrier(); } inline void OrderAccess::loadstore() { compiler_barrier(); } inline void OrderAccess::storeload() { fence(); } ![]()
static inline void compiler_barrier() { __asm__ volatile ("" : : : "memory");} inline void OrderAccess::loadload() { compiler_barrier(); }inline void OrderAccess::storestore() {compiler_barrier(); }inline void OrderAccess::loadstore() { compiler_barrier(); }inline void OrderAccess::storeload() { fence(); }從上面我們可以看出對除了對loadstore使用了fence()函數(shù),而loadload,loadstore, storestore都是使用的編譯器屏障.查看jdk1.8,發(fā)現(xiàn)loadload,loadstore對應(yīng)的acquire語境,storestore對應(yīng)的是release語境,這與之前介紹的是一致.
在jdk1.9中對storestore,storeload,loadload,loadstore的實現(xiàn)如下:
[plain] view plain copyprint?
inline void OrderAccess::acquire() { compiler_barrier(); } inline void OrderAccess::release() { compiler_barrier(); } inline void OrderAccess::fence() { if(os::is_MP()) { //always use locked addl since mfence is sometimes expensive #ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : :"cc", "memory"); #else __asm__ volatile ("lock; addl $0,0(%%esp)" : : :"cc", "memory"); #endif } compiler_barrier(); } ![]()
inline void OrderAccess::acquire() { compiler_barrier(); }inline void OrderAccess::release() { compiler_barrier(); } inline void OrderAccess::fence() { if(os::is_MP()) { //always use locked addl since mfence is sometimes expensive#ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : :"cc", "memory");#else __asm__ volatile ("lock; addl $0,0(%%esp)" : : :"cc", "memory");#endif } compiler_barrier();}可以發(fā)現(xiàn)acquire,release也是編譯器屏障,而fence的實現(xiàn)好像比較復(fù)雜.
os::is_MP()判斷是否是多核,如果是單核,那么就不存在內(nèi)存不可見或者亂序的問題,只要保證編譯器不亂序就好,所以使用編譯器屏障.
__asm__ volatile ("lock; addl$0,0(%%esp)" : : : "cc", "memory");
解釋一下:
__asm__ :代表匯編代碼開始.
__volatile__:禁止編譯器對代碼進行某些優(yōu)化.
Lock :匯編代碼,讓后面的操作是原子操作.lock指令會鎖住操作的緩存行(cacheline),一般用于read-Modify-write的操作例如 addl.“cc”,”mmeory”:cc代表的是寄存器,memory代表是內(nèi)存;這邊同時用了”cc”和”memory”,來通知編譯器內(nèi)存或者寄存器內(nèi)的內(nèi)容已經(jīng)發(fā)生了修改,要重新生成加載指令(不可以從緩存寄存器中取).
這邊的read/write請求不能越過lock指令進行重排,那么所有帶有l(wèi)ock prefix指令(lock ,xchgl等)都會構(gòu)成一個天然的x86 Mfence(讀寫屏障),這里用lock指令作為內(nèi)存屏障,然后利用asm volatile("" ::: "cc,memory")作為編譯器屏障.
AMD64這邊判斷是否是64位,64位機器中使用rsp棧指針寄存器,32位機器中使用32位機器中使用esp棧指針寄存器.
可以看到j(luò)vm開發(fā)組沒有使用x86的內(nèi)存屏障指令(mfence,lfence,sfence),個人覺得有以下原因:
1.x86/64的Intel平臺是強一致內(nèi)存模型,(能夠讓所有的處理器在任何時候任何指定的內(nèi)存地址上都可以看到完全相同的值).
2.x86/64的Intel平臺能夠保證讀讀,寫寫,讀寫這些是有序的.
基于以上兩個原因hotSpot對loadload,loadstore,storestore,只要利用編譯器屏障,保證程序不能重排就好.對于storeload屏障,利用了編譯器屏障,還使用了lock addl作為內(nèi)存屏障,原因是load,和store指令不能越過lock指令,形成了天然的MFence,沒有使用MFence是因為在某些平臺上,MFence的代價比較大.
4 淺析JVM中的volatile
4.1 c/c++的volatile
Java的volatile語義是從c/c++的volatile發(fā)展而來,并且進行增強,首先了解一下c/c++的volatile的語義.
c/c++中的volatile,能確保可見性。即:volatile變量的寫操作,不會緩存到寄存器中,而是直接回寫主存中去。而volatile變量的讀取,也不會從寄存器中讀取,而是從主存中讀取。這就是說c/C++volatile的第一特性:易變性。
c/c++的volatile的第二特性:不可優(yōu)化性。即不要對volatile變量進行各種激進的優(yōu)化,保持變量原有的語義,不能被優(yōu)化掉。
c/c++的volatile的第三特性:順序性。基于前面的兩個特性,讓Volatile經(jīng)常被解讀為一個為多線程而生的關(guān)鍵詞。但更要命的是很多程序員往往會忽略掉順序性,這使得c/c++ volatile很難被正確使用的重要原因。同時,c/c+的volatile是不能完全保證順序性的。
上面這段偽代碼,b變量被聲明為一個volatile的變量。在thread1去修改變量,而thread2去讀取變量。由于b變量聲明成一個volatile變量,在編譯器編譯時,不會從寄存器讀取變量,而是主存中去讀變量,同時也不會通過各種激進的優(yōu)化。即保證volatile的易變性,不可優(yōu)化行。即:thread1修改b變量,thread2就能讀取到變化的b變量。那么問題來了,上面這段偽代碼能否確保在b=1的情況下,a等于2。答案顯然是否定的。因為編譯器會對volatile變量與非volatile變量進行亂序優(yōu)化。即b=1,有可能在a=2前先執(zhí)行。
但是在a變量同時也申明成volatile,那么在編譯階段就能確保在thread1中a=2先執(zhí)行,b=1后執(zhí)行,不亂序。那么在thread2中,a能否確保看到等于2呢?答案是否定的。
因為即使c/c++volatile變量阻止了編譯器對volatile變量間的亂序優(yōu)化,但是終于生成的指令還是交給cpu來執(zhí)行的。CPU本身為了提高代碼運行的效率,也會對代碼的執(zhí)行順序進行調(diào)整,即cpu級別的亂序優(yōu)化。因此,即便把所有的變量全部都聲明為volatile,杜絕了編譯器的亂序優(yōu)化,但是針對生成的匯編代碼,CPU有可能仍舊會亂序執(zhí)行指令,導(dǎo)致程序依賴的邏輯出錯,c/c++的volatile對此無能為力。
其實,針對這個多線程的應(yīng)用,真正正確的做法,是構(gòu)建一個happens-before語義。也就是java對volatile的語義增強,可以防止volatile變量與普通變量或volatile變量之間的重排序,并且保證cpu執(zhí)行的時候不會重排.
4.2 java中的volatile
jSR-133專家組增強volatile的內(nèi)存語義:嚴(yán)格限制編譯器和處理器對volatile變量和普通變量之間的重排序,確保volatile的寫-讀和監(jiān)視器的釋放-獲取一樣,具有相同的內(nèi)存語義。
Java中的volatile在讀寫時候添加的內(nèi)存屏障如下:
//初始化
Int a = 1; volatile int v = 9;
Volatile的寫
Volatile的讀
Storestore;
v= a;
Storeload;
a = v;
loadload;
loadstore;
即:
release();
v= a;
fence();
即:
a = v;
acquire();
解釋一下,對volatile變量的寫,在前面插入storestore屏障(release語義),這樣就能保證所有在volatile變量寫之前寫的數(shù)據(jù),可以在volatile變量寫之前刷入主存,storeload這邊的可以將volatile變量刷入主存,還可以防止volatile變量寫之后又一個volatile變量的讀,會越過這個volatile變量的寫,保證volatile變量可以從內(nèi)存中讀數(shù)據(jù),而不是從storebuffer中讀取.
對于volatile變量的讀,在后面插入了loadload,loadstore,這樣就可以volatile變量之后的操作都不能重排到volatile變量的讀之前,同時保證了volatile變量之后的變量的讀,都是內(nèi)存中的最新數(shù)據(jù).
總結(jié):
1. 有volatile的標(biāo)識的變量具有可見性.對volatile變量的寫會被刷到主存到去,對volatile變量的讀,會直接從主存到讀,不會對volatile的變量進行任何的(寄存器的)緩存.
2. 任何變量的寫在volatile變量寫之前,那么這個變量在volatile變量讀之后可見的.
那樣只要在第一個例程中對第二個變量b加入volatile的標(biāo)識,那么就可以保證程序正確性了,這就是volatile變量的寫happen-before在volatile變量讀之前.
4.3 volatile與release_store_fence,release_store,load_acquire
release_store_fence,與load_acquire的具體代表什么意思?
/openjdk-jdk9-jdk9/hotspot/src/share/vm/runtime/orderAccess.inline.hpp文件中的
release_store_fence
inline void OrderAccess::release_store_fence(volatilejbyte* p, jbyte v) { specialized_release_store_fence(p, v);}
調(diào)用得了
template<typename T> inline voidOrderAccess::specialized_release_store_fence(volatile T* p, T v) { ordered_store<T, RELEASE_X_FENCE>(p,v); }
RELEASE_X_FENCE,這個有點像是在操作x之前插入了release,在操作x之后插入了fence?具體是不是這樣呢?
接著找到:
inline voidOrderAccess::ordered_store(volatile FieldType* p, FieldType v) {
ScopedFence<FenceType> f((void*)p);
store(p, v);
}
這段代碼定義了一個變量,然后進行存儲.神器之處就在于ScopedFence<FenceType>f((void*)p)的定義,
c++對一個類的變量的初始化,首先會調(diào)用構(gòu)造函數(shù),然后會在對變量進行回收的時候也就是調(diào)用結(jié)束的時候,調(diào)用析構(gòu)函數(shù).
可以看出構(gòu)造函數(shù)調(diào)用了prefix函數(shù),析構(gòu)函數(shù)調(diào)用了post函數(shù)
然后對于RELEASE_X_FENCE的中prefix和postfix的定義:
template<> inline voidScopedFenceGeneral<RELEASE_X_FENCE>::prefix() { OrderAccess::release(); }
template<> inline voidScopedFenceGeneral<RELEASE_X_FENCE>::postfix() {OrderAccess::fence(); }
如上:在寫之前調(diào)用了release即storestore,在寫之后調(diào)用了fence即storeload.
對于load_acquire相關(guān)源碼:
[java] view plain copyprint?
inline jbyte OrderAccess::load_acquire(volatilejbyte* p) { returnspecialized_load_acquire(p); } template<typename T> inline T OrderAccess::specialized_load_acquire (volatile T* p) { return ordered_load<T, X_ACQUIRE>(p); } template<> inline voidScopedFenceGeneral<X_ACQUIRE>::postfix() { OrderAccess::acquire(); } template <typename FieldType,ScopedFenceType FenceType> inline FieldTypeOrderAccess::ordered_load(volatile FieldType* p) { ScopedFence<FenceType> f((void*)p); return load(p); } ![]()
inline jbyte OrderAccess::load_acquire(volatilejbyte* p) { returnspecialized_load_acquire(p); }template<typename T> inline T OrderAccess::specialized_load_acquire (volatile T* p) { return ordered_load<T, X_ACQUIRE>(p); }template<> inline voidScopedFenceGeneral<X_ACQUIRE>::postfix() { OrderAccess::acquire(); }template <typename FieldType,ScopedFenceType FenceType>inline FieldTypeOrderAccess::ordered_load(volatile FieldType* p) { ScopedFence<FenceType> f((void*)p); return load(p);}從上代碼可知,load_acquire在讀之前調(diào)用了acquire即loadload或loadstore.
inline void OrderAccess::release_store(volatilejbyte* p, jbyte v) { specialized_release_store(p, v); }
template<typename T> inline voidOrderAccess::specialized_release_store (volatile T* p, T v) {ordered_store<T, RELEASE_X>(p, v); }
template<> inline voidScopedFenceGeneral<RELEASE_X>::prefix() { OrderAccess::release(); }
從上面代碼可知,releasestore只在store之前插入了release函數(shù).而并沒有插入storeload.
4.2 淺析volatile的源碼
在hotSpot中對volatile的實現(xiàn)的地方有多處,這里主要看的是從oops中的實現(xiàn).
找到oop中對volatile讀寫的實現(xiàn):
/openjdk-jdk9-jdk9/hotspot/src/share/vm/oops/oop.inline.hpp
[plain] view plain copyprint?
voidoopDesc::obj_field_put_volatile(int offset, oop value) { OrderAccess::release(); obj_field_put(offset, value); OrderAccess::fence(); } ![]()
voidoopDesc::obj_field_put_volatile(int offset, oop value) { OrderAccess::release(); obj_field_put(offset, value); OrderAccess::fence();}對volatile的字段寫之前插入了release,寫之后插入了fence,跟之前所描述是一致的.jint oopDesc::int_field_acquire(intoffset) const
{ returnOrderAccess::load_acquire(int_field_addr(offset)); }
從上文可知,load_acquire在volatile的讀之后插入acquire也就是loadload柵欄.
5.1 volatile與unsafe中的黑魔法
Unsafe.java里面有API類似getObjectVolatile,putObjectVolatile等,這兩個api具體有什么用處?
可以從名稱看出來這與volatile有關(guān).但是具體有什么作用,我們還是來查看源碼.
可以找到getObjectVolatile在unsafe.cpp中的實現(xiàn):
[java] view plain copyprint?
UNSAFE_ENTRY(jobject,Unsafe_GetObjectVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlongoffset)) UnsafeWrapper("Unsafe_GetObjectVolatile"); oop p = JNIHandles::resolve(obj); void* addr =index_oop_from_field_offset_long(p, offset); volatile oop v; if (UseCompressedOops) { volatile narrowOop n = *(volatilenarrowOop*) addr; (void)const_cast<oop&>(v =oopDesc::decode_heap_oop(n)); } else { (void)const_cast<oop&>(v =*(volatile oop*) addr); } OrderAccess::acquire(); return JNIHandles::make_local(env, v); UNSAFE_END ![]()
UNSAFE_ENTRY(jobject,Unsafe_GetObjectVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlongoffset)) UnsafeWrapper("Unsafe_GetObjectVolatile"); oop p = JNIHandles::resolve(obj); void* addr =index_oop_from_field_offset_long(p, offset); volatile oop v; if (UseCompressedOops) { volatile narrowOop n = *(volatilenarrowOop*) addr; (void)const_cast<oop&>(v =oopDesc::decode_heap_oop(n)); } else { (void)const_cast<oop&>(v =*(volatile oop*) addr); } OrderAccess::acquire(); return JNIHandles::make_local(env, v);UNSAFE_END我們來看看到代碼volatile oopv; 定義了一個c++的volatile,進行一個讀取操作;c++ volatile具有的語義是每次都能夠從內(nèi)存中看到的最新的,oop這個類是java中的對象類在c++中對應(yīng)的實現(xiàn)類,即使用c++的語義讀取了這個對象的引用地址,保證了可見性.而后是acquire指令.
OrderAccess::acquire();
從上文可知acquire是有l(wèi)oadload,和loadStore組成的這就是volatile變量所要插入的柵欄,并且可以保證,acquire之后的讀都是具有可見性的,x86_64在jdk9中是有編譯器柵欄實現(xiàn)的,可以推斷出,任意的一個變量通過getObjectVolatile是可以獲取javavolatile的讀的語義的.
找到putObjectVolatile在unsafe.cpp中的具體實現(xiàn),
[cpp] view plain copyprint?
UNSAFE_ENTRY(void,Unsafe_SetObjectVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlongoffset, jobject x_h)) UnsafeWrapper("Unsafe_SetObjectVolatile"); oop x = JNIHandles::resolve(x_h); oop p = JNIHandles::resolve(obj); void* addr =index_oop_from_field_offset_long(p, offset); OrderAccess::release(); if (UseCompressedOops) { oop_store((narrowOop*)addr, x); } else { oop_store((oop*)addr, x); } OrderAccess::fence(); UNSAFE_END ![]()
UNSAFE_ENTRY(void,Unsafe_SetObjectVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlongoffset, jobject x_h)) UnsafeWrapper("Unsafe_SetObjectVolatile"); oop x = JNIHandles::resolve(x_h); oop p = JNIHandles::resolve(obj); void* addr =index_oop_from_field_offset_long(p, offset); OrderAccess::release(); if (UseCompressedOops) { oop_store((narrowOop*)addr, x); } else { oop_store((oop*)addr, x); } OrderAccess::fence();UNSAFE_ENDopp_store是JavaObject變量的寫,在這之前插入了release,在之后插入了fence,這邊又有了javavolatile寫的語義,因為release對應(yīng)的loadStore,storestore的兩個柵欄;fence對應(yīng)的是storeload柵欄,這與java volatile的寫所需要的柵欄是一致的.
總結(jié):一個引用要變成volatile的讀的語義,只要用getObjectVolatile,一個引用要想有volatile寫的語義,主要putObjectvolatile.
5.2 volatile的相關(guān)特性
5.2 volatile的寫
從上文可知道,volatile的寫所插入的storeLoad是一個耗時的操作,所以出現(xiàn)了一個對volatile寫的升級版本.
AtomicIntegeri=new AtomicInteger(5);
i.lazySet(5);
如上利用laySet方法進行性能優(yōu)化,在實現(xiàn)上對volatile的寫只會在之前插入storestore柵欄,這樣可以保證的volatile寫之前的寫都不能重排到volatile寫之后,volatile之前的寫都會在volatile變量寫之前刷入主存,但是這個方法雖然提高了效率,但是volatile的寫并不能被馬上被其它線程看到,通常需要幾納秒才能被其它線程看到,這個時間比較短,所以代價是可以忍受的.
5.2.2 volatile的讀
網(wǎng)上流傳著volatile的讀與普通變量是差不多的性能,但是如果你讀了本文,你就知道,volatile的讀其實也是有代價,而且相比普通的變量的讀要慢的多.原因是,volatile的變量不僅要從內(nèi)存中讀,還使得它之后的所讀的變量也是從內(nèi)存中讀的.
為此常常將循環(huán)中對volatile的變量的讀,提取到循環(huán)之外.
Volatile int I= 0;
For(int j = 0;j < 100000;j++){
Int p = I;
}
優(yōu)化為
Int p = I;
For(int j = 0;j < 100000;j++){
//直接操作p
}
如上,這樣就可以使得volatile變量緩存在寄存器中,那么就不用每次都去從主存中去讀取,這樣就可以大大的提高性能.
5.2.3 volatile與原子性
Volatile標(biāo)識的變量的純粹的讀和寫都是原子,普通long
1.引言
如果你對Java的volatile有著疑慮請閱讀本文,如果你想對volatile想有一個更深的了解,請閱讀本文.本文主要講的是volatile的寫happen-before在volatile讀之前所涉及相關(guān)的原理,以及在Hotspot中相關(guān)代碼的實現(xiàn).
首先從一段代碼開始講起,如下
初始化
[plain] view plain copyprint?
int a = 0, int b = 0; void foo(void) { a= 1; b= 1; } void bar(void) { while (b == 0) continue; If(a == 1) { System.out.println(“true”); }; } ![]()
int a = 0, int b = 0; void foo(void) { a= 1; b= 1; } void bar(void) { while (b == 0) continue; If(a == 1) { System.out.println(“true”); }; }以上的代碼,threadA運行foo方法,threadB運行bar方法;在threadB中,能否在while循環(huán)跳出之后,即b=1的情況下,得到a一定等于1呢?答案是否定的.因為存在著編譯器的亂序和cpu指令的亂序,程序沒有按照我們所寫的順序執(zhí)行.
2.1編譯器亂序
編譯器是如何亂序的?編譯器在不改變單線程語義的前提之下,為了提高程序的運行速度,可以對指令進行亂序.
編譯器按照一個原則,進行亂序:不能改變單線程程序的行為.
在不改變單線程運行的結(jié)果,以上foo函數(shù)就有可能有兩種編譯結(jié)果:
第一種:
a = 1;
b= 1;
第二種:
b = 1;
a = 1;
如上,如果cpu按照第二種編譯結(jié)果執(zhí)行,那么就不能正確的輸出”true”.雖然亂序了,但是并沒有改變單線程threadA執(zhí)行foo的語義,所以上面的重排是允許的.
2.2.cpu亂序
2.2.1 cpu的結(jié)構(gòu)與cpu亂序:
首先來了解一下x86 cpu的結(jié)構(gòu):
c1,c2,c3 .. cn是160個用于整數(shù)和144個用于浮點的寄存器單元,用于存儲本地變量和函數(shù)參數(shù).cpu訪問寄存器的需要1cycle< 1ns,cpu訪問寄存器是最快的.
LoadBuffer,storeBuffer合稱排序緩沖(Memoryordering Buffers (MOB)),Load緩沖有64長度,store緩沖有36長度.buffer與L1進行數(shù)據(jù)傳輸,cpu無須等待.
L1是本地核心內(nèi)的緩存,被分成獨立的32K的數(shù)據(jù)緩存和32k指令緩存.訪問L1需要3cycles ~1ns.
L2緩存是本地核心內(nèi)的緩存,被設(shè)計為L1緩存與共享的L3緩存之間的緩沖,L2緩存大小為256K.訪問L2需要12cycles-3ns.
L3:在同插槽的所有核心共享L3緩存,L3緩存被分為數(shù)個2M的段,訪問L3需要38cycles~12ns.
DRAM:就是我們通常說的內(nèi)存,訪問內(nèi)存一般需要65ns.
cpu加載不同緩存級別的數(shù)據(jù),從上往下,需要的時間是越來越多的,而等待對cpu來說是極大的浪費;對于不同的插槽的cpu所擁有的一級二級緩存是不共享的,而不同的緩存之間需要保證一致性,主要通過MESI協(xié)議,為了保證不同緩存的一致性也是需要付出時間的,因為不同狀態(tài)之間的轉(zhuǎn)換需要等待消息的回復(fù),這個等待過程往往是阻塞的.
根據(jù)MESI協(xié)議,變量要從S狀態(tài)變?yōu)镸狀態(tài),本cpu要執(zhí)行read andmodify ,需要發(fā)出Invalidate消息,然后等待,待收到所有其它cpu的回復(fù)之后才執(zhí)行更新狀態(tài),其他操作也是如此,都有會等待的過程。這些過程都是阻塞的動作,無疑會給cpu的性能帶來巨大的損耗。
經(jīng)過不斷地改進,在寄存器與cache之間加上loadbuffer、storebuffer,來減小這些阻塞的時間。cpu要讀取數(shù)據(jù)時,先把讀請求發(fā)到loadbuffer中,無需等待其他cpu的響應(yīng),就可以先進行下一步操作。直到其他cpu的響應(yīng)結(jié)果達(dá)到之后,再處理這個讀請求的結(jié)果。cpu寫數(shù)據(jù)時,就把數(shù)據(jù)寫到storebuffer中,此時cpu認(rèn)為已經(jīng)把數(shù)據(jù)寫出去了,待到某個適合的時間點(cpu空閑時間),在把storebuffer的數(shù)據(jù)刷到主存中去。
根據(jù)以上的描述,可知在storebuffer中的數(shù)據(jù)有臨時可見性問題。即在storebuffer中的數(shù)據(jù)對本cpu是可見的(本cpu讀取數(shù)據(jù)時,可以直接從storebuffer中進行讀取),其它槽的cpu對storebuffer中的數(shù)據(jù)不可見。loadbuffer中的請求無法取到其他cpu修改的最新數(shù)據(jù),因為最新數(shù)據(jù)在其他cpu的storebuffer中。同時,在loadbuffer、storebuffer中的"請求"的完成都是異步的,即表現(xiàn)為它們完成的順序是不確定的,指令的亂序。
2.2.2 cpu亂序
cpu亂序的一種情況:
Processor 0
Processor 1
mov [ _x], 1
mov [ _y], 1
mov r1, [ _y]
mov r2, [_x]
Initially x == y == 0
解釋一下:x,y表示內(nèi)存中的變量,r1,r2表示寄存器, mov [_x],1,表示把1賦值給x變量,從順序執(zhí)行的角度來說,結(jié)果肯定是r1=1,或r2 =1;然而r1=0并且r2=0是存在的.
cpu實際執(zhí)行的情況可能如下
Processor 0
Processor 1
mov r1, [ _y] // (1)
mov r2, [_x] // (2)
mov [ _x], 1 // (3)
mov [ _y], 1 // (4)
如上(1),(2),(3),(4)的順序執(zhí)行,便會得到r1=0,r2=0.可以這么理解,大部分的處理器,在保證單線程執(zhí)行語義正確的基礎(chǔ)上,會根據(jù)一定的規(guī)則對指令進行重排.
為什么會出現(xiàn)上面的情況?
因為cpu會將寫放入storebuffer中,然后立刻返回不會等待,會將讀放入load buffer中,然后立刻返回,這兩個隊列都是異步的,他們寫讀的是不同地址,不存在依賴,兩者誰先完成是不知道的,如果讀指令先完成了,就出現(xiàn)上面的重排的情況.
3. 編譯器屏障與內(nèi)存屏障
由于存在著編譯器對程序指令的重排,這個需要編譯器屏障來保證程序的正確執(zhí)行,cpu也會對指令進行重排,這個需要內(nèi)存屏障來保證.所以屏障需要對編譯器和內(nèi)存同時起作用,來保證程序可以正確執(zhí)行.
因為我們大部分的軟件主要都是在x86_linux上運行的,所以我們主要研究的是x86的相關(guān)編譯器屏障和內(nèi)存屏障.
3.1 編譯屏障
Open jdk9中x86_linux的柵欄指令編寫如下:
static inline void compiler_barrier() {
__asm__ volatile ("" : : : "memory"); /*編譯器屏障*/
}
此處 __asm__ volatile("" : : : "memory"); 是內(nèi)嵌匯編.
解釋:
__asm__ :代表匯編代碼開始.
__volatile__:禁止編譯器對代碼進行某些優(yōu)化.
memory: memory代表是內(nèi)存;這邊用”memory”,來通知編譯器內(nèi)存的內(nèi)容已經(jīng)發(fā)生了修改,要重新生成加載指令(不可以從緩存寄存器中取).因為存在著內(nèi)存的改變,不可以將前后的代碼進行亂序.
asm volatile("" :::"memory"),這句內(nèi)嵌匯編作為編譯器屏障,可以防止編譯器對相鄰指令進行亂序,但是它無法阻止CPU的亂序;也就是說它僅僅禁止了編譯器的亂序優(yōu)化,不會阻止CPU的亂序執(zhí)行。
以下利用asm volatile("" ::: "memory")來驗證,對于屏障之后的讀,都是讀取的主存,而不是從寄存器中讀取的.當(dāng)然如果您不懂匯編和c++這部分您可以跳過.
實驗一:
[java] view plain copyprint?
#include <stdio.h> int foo = 10; int bar = 15; int main(void) { int ss = foo + 1; __asm__ __volatile__("":::"memory"); int foo1 = foo + 2; printf("ss=%d/n", ss); printf("foo1=%d/n", foo1); return0; } ![]()
#include <stdio.h>int foo = 10;int bar = 15;int main(void){ int ss = foo + 1; __asm__ __volatile__("":::"memory"); int foo1 = foo + 2; printf("ss=%d/n", ss); printf("foo1=%d/n", foo1); return0;}編譯命令:
g++ -S -O2 test1.cpp
編譯結(jié)果的部分匯編:
[plain] view plain copyprint?
main: .LFB30: .cfi_startproc movl foo(%rip), %eax //將foo變量從內(nèi)存加載到寄存器eax中 pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 leal 1(%rax), %edx //將rax變量加1賦值給edx寄存器也是ss變量. //rax是64位寄存器,它的低32位是eax寄存器. movl foo(%rip), %eax//將foo變量從內(nèi)存加載到寄存器eax中 movl $.LC0, %esi movl $1, %edi leal 2(%rax), %ebx//將rax變量加2賦值給edx寄存器也是foo1變量. xorl %eax, %eax call __printf_chk movl %ebx, %edx movl $.LC1, %esi movl $1, %edi xorl %eax, %eax call __printf_chk xorl %eax, %eax popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc ![]()
main:.LFB30: .cfi_startproc movl foo(%rip), %eax //將foo變量從內(nèi)存加載到寄存器eax中 pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 leal 1(%rax), %edx //將rax變量加1賦值給edx寄存器也是ss變量. //rax是64位寄存器,它的低32位是eax寄存器. movl foo(%rip), %eax//將foo變量從內(nèi)存加載到寄存器eax中 movl $.LC0, %esi movl $1, %edi leal 2(%rax), %ebx//將rax變量加2賦值給edx寄存器也是foo1變量. xorl %eax, %eax call __printf_chk movl %ebx, %edx movl $.LC1, %esi movl $1, %edi xorl %eax, %eax call __printf_chk xorl %eax, %eax popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc實驗二:
[cpp] view plain copyprint?
#include <stdio.h> int foo = 10; int bar = 15; int main(void) { int ss = foo + 1; int foo1 = foo + 2; printf("ss=%d/n", ss); printf("foo1=%d/n", foo1); return0; } ![]()
#include <stdio.h>int foo = 10;int bar = 15;int main(void){ int ss = foo + 1; int foo1 = foo + 2; printf("ss=%d/n", ss); printf("foo1=%d/n", foo1); return0;}編譯命令:
g++ -S -O2 test2.cpp
編譯結(jié)果的部分匯編:
[plain] view plain copyprint?
main: FB30: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movl foo(%rip), %ebx //將foo變量從內(nèi)存加載到寄存器ebx中 movl $.LC0, %esi movl $1, %edi xorl %eax, %eax leal 1(%rbx), %edx//將rbx寄存器加1賦值給edx寄存器也是ss變量. //rbx是64位寄存器,它的低32位是ebx寄存器. call __printf_chk leal 2(%rbx), %edx//直接取rbx寄存器,加2賦值給foo1變量 movl $.LC1, %esi movl $1, %edi xorl %eax, %eax call __printf_chk xorl %eax, %eax popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc ![]()
main:.LFB30: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movl foo(%rip), %ebx //將foo變量從內(nèi)存加載到寄存器ebx中 movl $.LC0, %esi movl $1, %edi xorl %eax, %eax leal 1(%rbx), %edx//將rbx寄存器加1賦值給edx寄存器也是ss變量. //rbx是64位寄存器,它的低32位是ebx寄存器. call __printf_chk leal 2(%rbx), %edx//直接取rbx寄存器,加2賦值給foo1變量 movl $.LC1, %esi movl $1, %edi xorl %eax, %eax call __printf_chk xorl %eax, %eax popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc
結(jié)論:第一實驗加入了內(nèi)存屏障中,兩次分別從內(nèi)存中加載foo變量到寄存器中,然后進行操作,第二個實驗中取出了內(nèi)存屏障,第一次將foo變量從內(nèi)存中加載到了寄存器中,進行第一次操作,然后第二次并沒有從內(nèi)存中加載,而是直接去寄存器中的值去操作,由此可以看出屏障的后面是不可以緩存變量在寄存器中的,而是屏障后面的變量是需要重新從主存中加載的.
3.2 Java的內(nèi)存屏障
從上文可知:一個load操作需要進入loadbuffer中的,然后在從內(nèi)存中去加載,一個store操作需要進入storebuffer然后在寫入內(nèi)存.而兩個buffer之間是異步,導(dǎo)致出現(xiàn)了不同的亂序(重排),java定義了一系列的內(nèi)存屏障來指定指令的執(zhí)行順序.
Java中的內(nèi)存中的內(nèi)存屏障:
LoadLoad 屏障
StoreStore屏障
LoadStore 屏障
StoreLoad屏障
序列
Load1,Loadload,Load2
Store1,StoreStore,Store2
Load1,LoadStore,Store
Store1,StoreLoad,Load
作用
保證Load1所要讀入的數(shù)據(jù)能夠在被Load2和后續(xù)的load指令訪問前讀入。
保證Store1的數(shù)據(jù)在Store2以及后續(xù)Store指令操作相關(guān)數(shù)據(jù)之前對其它處理器可見
確保Load1的數(shù)據(jù)在Store2和后續(xù)Store指令被刷新之前讀取。
確保Store1的數(shù)據(jù)在被Load2和后續(xù)的Load指令讀取之前對其他處理器可見。
對buffer的影響
在Load buffer插入屏障,清空屏障之前的Load操作,然后才能執(zhí)行屏障之后的Load操作.
在Store buffer插入屏障,清空屏障之前的Store操作,然后才能執(zhí)行屏障之后的store操作.
在Load buffer插入屏障,清空屏障之前的Load操作,然后才能執(zhí)行屏障之后的Store操作.
在Load buffer, Store buffer中都插入屏障,必須清空屏障之前的Load操作并且清空屏障之前的store操作,然后才能執(zhí)行屏障之后的Load操作,或store操作.
StoreLoad屏障有可以同時獲得其它三種屏障(loadload,loadstore,storestore)的的效果,但是StoreLoad是一個比較耗性能的屏障.因為StoreLoad屏障在Loadbuffer, Store buffer中都插入屏障,必須清空屏障之前的Load操作并且清空屏障之前的store操作,然后才能執(zhí)行屏障之后的Load操作,或store操作.這使得之后的讀指令不能從store buffer中直接獲取,只能從緩存中獲取,綜合起來說,storeload屏障最耗性能。
3.2 Release 和Acquire,fence
Java中又定義了release和acquire,fence三種不同的語境的內(nèi)存柵欄.
如上圖,loadLoad和loadStore兩種柵欄對應(yīng)的都是acquire語境,,acquire語境一般定義在java的讀之前;在編譯器階段和cpu執(zhí)行的時候,acquire之后的所有的(讀和寫)操作不能越過acquire,重排到acquire之前,acquire指令之后所有的讀都是具有可見性的.
如上圖,StoreStore和LoadStore對應(yīng)的是release語境,release語境一般定義在java的寫之后,在編譯器和cpu執(zhí)行的時候,所有release之前的所有的(讀和寫)操作都不能越過release,重排到release之后,release指令之前所有的寫都會刷新到主存中去,其他核的cpu可以看到刷新的最新值.
對于fence,是由storeload柵欄組成的,比較消耗性能.在編譯器階段和cpu執(zhí)行時候,保證fence之前的任何操作不能重排到屏障之后,fence之后的任何操作不能重排到屏障之前.fence具有acquire和release這兩個都有的語境,即可以將fence之前的寫刷新到內(nèi)存中,fence之后的讀都是具有可見性的.
3.3 x86中的內(nèi)存柵欄與實現(xiàn)
內(nèi)存屏障,也稱內(nèi)存柵欄,內(nèi)存柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對內(nèi)存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點之后的操作。語義上,內(nèi)存屏障之前的所有寫操作都要寫入主存;內(nèi)存屏障之后的讀操作,直接讀取的主存,可以獲得內(nèi)存屏障之前的寫操作的結(jié)果。
完全內(nèi)存屏障(full memory barrier)保障了早于屏障的內(nèi)存讀寫操作的結(jié)果提交到內(nèi)存之后,再執(zhí)行晚于屏障的讀寫操作,在loadbuffer和storebuffer中插入屏障,清空屏障之前的讀和寫操作。X86中對應(yīng)MFence;
內(nèi)存讀屏障(read memory barrier)僅確保了內(nèi)存讀操作.在loadbuffe中插入屏障,清空屏障之前的讀操作;LFence
內(nèi)存寫屏障(write memory barrier)僅保證了內(nèi)存寫操作.在storebuffer中插入屏障,清空屏障之的寫操作; SFence
接下來我們看看在源碼中如何實現(xiàn),主要看open jdk1.9,為什么,jdk1.8里面對柵欄的實現(xiàn)千奇百怪,作者能力有限,不能全部了解jvm開發(fā)者當(dāng)時為啥那么實現(xiàn),而jdk1.9的實現(xiàn)相對比比較清晰.
在jdk1.9中對storestore,storeload,loadload,loadstore的實現(xiàn)如下:
[plain] view plain copyprint?
static inline void compiler_barrier() { __asm__ volatile ("" : : : "memory"); } inline void OrderAccess::loadload() { compiler_barrier(); } inline void OrderAccess::storestore() {compiler_barrier(); } inline void OrderAccess::loadstore() { compiler_barrier(); } inline void OrderAccess::storeload() { fence(); } ![]()
static inline void compiler_barrier() { __asm__ volatile ("" : : : "memory");} inline void OrderAccess::loadload() { compiler_barrier(); }inline void OrderAccess::storestore() {compiler_barrier(); }inline void OrderAccess::loadstore() { compiler_barrier(); }inline void OrderAccess::storeload() { fence(); }從上面我們可以看出對除了對loadstore使用了fence()函數(shù),而loadload,loadstore, storestore都是使用的編譯器屏障.查看jdk1.8,發(fā)現(xiàn)loadload,loadstore對應(yīng)的acquire語境,storestore對應(yīng)的是release語境,這與之前介紹的是一致.
在jdk1.9中對storestore,storeload,loadload,loadstore的實現(xiàn)如下:
[plain] view plain copyprint?
inline void OrderAccess::acquire() { compiler_barrier(); } inline void OrderAccess::release() { compiler_barrier(); } inline void OrderAccess::fence() { if(os::is_MP()) { //always use locked addl since mfence is sometimes expensive #ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : :"cc", "memory"); #else __asm__ volatile ("lock; addl $0,0(%%esp)" : : :"cc", "memory"); #endif } compiler_barrier(); } ![]()
inline void OrderAccess::acquire() { compiler_barrier(); }inline void OrderAccess::release() { compiler_barrier(); } inline void OrderAccess::fence() { if(os::is_MP()) { //always use locked addl since mfence is sometimes expensive#ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : :"cc", "memory");#else __asm__ volatile ("lock; addl $0,0(%%esp)" : : :"cc", "memory");#endif } compiler_barrier();}可以發(fā)現(xiàn)acquire,release也是編譯器屏障,而fence的實現(xiàn)好像比較復(fù)雜.
os::is_MP()判斷是否是多核,如果是單核,那么就不存在內(nèi)存不可見或者亂序的問題,只要保證編譯器不亂序就好,所以使用編譯器屏障.
__asm__ volatile ("lock; addl$0,0(%%esp)" : : : "cc", "memory");
解釋一下:
__asm__ :代表匯編代碼開始.
__volatile__:禁止編譯器對代碼進行某些優(yōu)化.
Lock :匯編代碼,讓后面的操作是原子操作.lock指令會鎖住操作的緩存行(cacheline),一般用于read-Modify-write的操作例如 addl.“cc”,”mmeory”:cc代表的是寄存器,memory代表是內(nèi)存;這邊同時用了”cc”和”memory”,來通知編譯器內(nèi)存或者寄存器內(nèi)的內(nèi)容已經(jīng)發(fā)生了修改,要重新生成加載指令(不可以從緩存寄存器中取).
這邊的read/write請求不能越過lock指令進行重排,那么所有帶有l(wèi)ock prefix指令(lock ,xchgl等)都會構(gòu)成一個天然的x86 Mfence(讀寫屏障),這里用lock指令作為內(nèi)存屏障,然后利用asm volatile("" ::: "cc,memory")作為編譯器屏障.
AMD64這邊判斷是否是64位,64位機器中使用rsp棧指針寄存器,32位機器中使用32位機器中使用esp棧指針寄存器.
可以看到j(luò)vm開發(fā)組沒有使用x86的內(nèi)存屏障指令(mfence,lfence,sfence),個人覺得有以下原因:
1.x86/64的Intel平臺是強一致內(nèi)存模型,(能夠讓所有的處理器在任何時候任何指定的內(nèi)存地址上都可以看到完全相同的值).
2.x86/64的Intel平臺能夠保證讀讀,寫寫,讀寫這些是有序的.
基于以上兩個原因hotSpot對loadload,loadstore,storestore,只要利用編譯器屏障,保證程序不能重排就好.對于storeload屏障,利用了編譯器屏障,還使用了lock addl作為內(nèi)存屏障,原因是load,和store指令不能越過lock指令,形成了天然的MFence,沒有使用MFence是因為在某些平臺上,MFence的代價比較大.
4 淺析JVM中的volatile
4.1 c/c++的volatile
Java的volatile語義是從c/c++的volatile發(fā)展而來,并且進行增強,首先了解一下c/c++的volatile的語義.
c/c++中的volatile,能確保可見性。即:volatile變量的寫操作,不會緩存到寄存器中,而是直接回寫主存中去。而volatile變量的讀取,也不會從寄存器中讀取,而是從主存中讀取。這就是說c/C++volatile的第一特性:易變性。
c/c++的volatile的第二特性:不可優(yōu)化性。即不要對volatile變量進行各種激進的優(yōu)化,保持變量原有的語義,不能被優(yōu)化掉。
c/c++的volatile的第三特性:順序性。基于前面的兩個特性,讓Volatile經(jīng)常被解讀為一個為多線程而生的關(guān)鍵詞。但更要命的是很多程序員往往會忽略掉順序性,這使得c/c++ volatile很難被正確使用的重要原因。同時,c/c+的volatile是不能完全保證順序性的。
上面這段偽代碼,b變量被聲明為一個volatile的變量。在thread1去修改變量,而thread2去讀取變量。由于b變量聲明成一個volatile變量,在編譯器編譯時,不會從寄存器讀取變量,而是主存中去讀變量,同時也不會通過各種激進的優(yōu)化。即保證volatile的易變性,不可優(yōu)化行。即:thread1修改b變量,thread2就能讀取到變化的b變量。那么問題來了,上面這段偽代碼能否確保在b=1的情況下,a等于2。答案顯然是否定的。因為編譯器會對volatile變量與非volatile變量進行亂序優(yōu)化。即b=1,有可能在a=2前先執(zhí)行。
但是在a變量同時也申明成volatile,那么在編譯階段就能確保在thread1中a=2先執(zhí)行,b=1后執(zhí)行,不亂序。那么在thread2中,a能否確保看到等于2呢?答案是否定的。
因為即使c/c++volatile變量阻止了編譯器對volatile變量間的亂序優(yōu)化,但是終于生成的指令還是交給cpu來執(zhí)行的。CPU本身為了提高代碼運行的效率,也會對代碼的執(zhí)行順序進行調(diào)整,即cpu級別的亂序優(yōu)化。因此,即便把所有的變量全部都聲明為volatile,杜絕了編譯器的亂序優(yōu)化,但是針對生成的匯編代碼,CPU有可能仍舊會亂序執(zhí)行指令,導(dǎo)致程序依賴的邏輯出錯,c/c++的volatile對此無能為力。
其實,針對這個多線程的應(yīng)用,真正正確的做法,是構(gòu)建一個happens-before語義。也就是java對volatile的語義增強,可以防止volatile變量與普通變量或volatile變量之間的重排序,并且保證cpu執(zhí)行的時候不會重排.
4.2 java中的volatile
jSR-133專家組增強volatile的內(nèi)存語義:嚴(yán)格限制編譯器和處理器對volatile變量和普通變量之間的重排序,確保volatile的寫-讀和監(jiān)視器的釋放-獲取一樣,具有相同的內(nèi)存語義。
Java中的volatile在讀寫時候添加的內(nèi)存屏障如下:
//初始化
Int a = 1; volatile int v = 9;
Volatile的寫
Volatile的讀
Storestore;
v= a;
Storeload;
a = v;
loadload;
loadstore;
即:
release();
v= a;
fence();
即:
a = v;
acquire();
解釋一下,對volatile變量的寫,在前面插入storestore屏障(release語義),這樣就能保證所有在volatile變量寫之前寫的數(shù)據(jù),可以在volatile變量寫之前刷入主存,storeload這邊的可以將volatile變量刷入主存,還可以防止volatile變量寫之后又一個volatile變量的讀,會越過這個volatile變量的寫,保證volatile變量可以從內(nèi)存中讀數(shù)據(jù),而不是從storebuffer中讀取.
對于volatile變量的讀,在后面插入了loadload,loadstore,這樣就可以volatile變量之后的操作都不能重排到volatile變量的讀之前,同時保證了volatile變量之后的變量的讀,都是內(nèi)存中的最新數(shù)據(jù).
總結(jié):
1. 有volatile的標(biāo)識的變量具有可見性.對volatile變量的寫會被刷到主存到去,對volatile變量的讀,會直接從主存到讀,不會對volatile的變量進行任何的(寄存器的)緩存.
2. 任何變量的寫在volatile變量寫之前,那么這個變量在volatile變量讀之后可見的.
那樣只要在第一個例程中對第二個變量b加入volatile的標(biāo)識,那么就可以保證程序正確性了,這就是volatile變量的寫happen-before在volatile變量讀之前.
4.3 volatile與release_store_fence,release_store,load_acquire
release_store_fence,與load_acquire的具體代表什么意思?
/openjdk-jdk9-jdk9/hotspot/src/share/vm/runtime/orderAccess.inline.hpp文件中的
release_store_fence
inline void OrderAccess::release_store_fence(volatilejbyte* p, jbyte v) { specialized_release_store_fence(p, v);}
調(diào)用得了
template<typename T> inline voidOrderAccess::specialized_release_store_fence(volatile T* p, T v) { ordered_store<T, RELEASE_X_FENCE>(p,v); }
RELEASE_X_FENCE,這個有點像是在操作x之前插入了release,在操作x之后插入了fence?具體是不是這樣呢?
接著找到:
inline voidOrderAccess::ordered_store(volatile FieldType* p, FieldType v) {
ScopedFence<FenceType> f((void*)p);
store(p, v);
}
這段代碼定義了一個變量,然后進行存儲.神器之處就在于ScopedFence<FenceType>f((void*)p)的定義,
c++對一個類的變量的初始化,首先會調(diào)用構(gòu)造函數(shù),然后會在對變量進行回收的時候也就是調(diào)用結(jié)束的時候,調(diào)用析構(gòu)函數(shù).
可以看出構(gòu)造函數(shù)調(diào)用了prefix函數(shù),析構(gòu)函數(shù)調(diào)用了post函數(shù)
然后對于RELEASE_X_FENCE的中prefix和postfix的定義:
template<> inline voidScopedFenceGeneral<RELEASE_X_FENCE>::prefix() { OrderAccess::release(); }
template<> inline voidScopedFenceGeneral<RELEASE_X_FENCE>::postfix() {OrderAccess::fence(); }
如上:在寫之前調(diào)用了release即storestore,在寫之后調(diào)用了fence即storeload.
對于load_acquire相關(guān)源碼:
[java] view plain copyprint?
inline jbyte OrderAccess::load_acquire(volatilejbyte* p) { returnspecialized_load_acquire(p); } template<typename T> inline T OrderAccess::specialized_load_acquire (volatile T* p) { return ordered_load<T, X_ACQUIRE>(p); } template<> inline voidScopedFenceGeneral<X_ACQUIRE>::postfix() { OrderAccess::acquire(); } template <typename FieldType,ScopedFenceType FenceType> inline FieldTypeOrderAccess::ordered_load(volatile FieldType* p) { ScopedFence<FenceType> f((void*)p); return load(p); } ![]()
inline jbyte OrderAccess::load_acquire(volatilejbyte* p) { returnspecialized_load_acquire(p); }template<typename T> inline T OrderAccess::specialized_load_acquire (volatile T* p) { return ordered_load<T, X_ACQUIRE>(p); }template<> inline voidScopedFenceGeneral<X_ACQUIRE>::postfix() { OrderAccess::acquire(); }template <typename FieldType,ScopedFenceType FenceType>inline FieldTypeOrderAccess::ordered_load(volatile FieldType* p) { ScopedFence<FenceType> f((void*)p); return load(p);}從上代碼可知,load_acquire在讀之前調(diào)用了acquire即loadload或loadstore.
inline void OrderAccess::release_store(volatilejbyte* p, jbyte v) { specialized_release_store(p, v); }
template<typename T> inline voidOrderAccess::specialized_release_store (volatile T* p, T v) {ordered_store<T, RELEASE_X>(p, v); }
template<> inline voidScopedFenceGeneral<RELEASE_X>::prefix() { OrderAccess::release(); }
從上面代碼可知,releasestore只在store之前插入了release函數(shù).而并沒有插入storeload.
4.2 淺析volatile的源碼
在hotSpot中對volatile的實現(xiàn)的地方有多處,這里主要看的是從oops中的實現(xiàn).
找到oop中對volatile讀寫的實現(xiàn):
/openjdk-jdk9-jdk9/hotspot/src/share/vm/oops/oop.inline.hpp
[plain] view plain copyprint?
voidoopDesc::obj_field_put_volatile(int offset, oop value) { OrderAccess::release(); obj_field_put(offset, value); OrderAccess::fence(); } ![]()
voidoopDesc::obj_field_put_volatile(int offset, oop value) { OrderAccess::release(); obj_field_put(offset, value); OrderAccess::fence();}對volatile的字段寫之前插入了release,寫之后插入了fence,跟之前所描述是一致的.jint oopDesc::int_field_acquire(intoffset) const
{ returnOrderAccess::load_acquire(int_field_addr(offset)); }
從上文可知,load_acquire在volatile的讀之后插入acquire也就是loadload柵欄.
5.1 volatile與unsafe中的黑魔法
Unsafe.java里面有API類似getObjectVolatile,putObjectVolatile等,這兩個api具體有什么用處?
可以從名稱看出來這與volatile有關(guān).但是具體有什么作用,我們還是來查看源碼.
可以找到getObjectVolatile在unsafe.cpp中的實現(xiàn):
[java] view plain copyprint?
UNSAFE_ENTRY(jobject,Unsafe_GetObjectVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlongoffset)) UnsafeWrapper("Unsafe_GetObjectVolatile"); oop p = JNIHandles::resolve(obj); void* addr =index_oop_from_field_offset_long(p, offset); volatile oop v; if (UseCompressedOops) { volatile narrowOop n = *(volatilenarrowOop*) addr; (void)const_cast<oop&>(v =oopDesc::decode_heap_oop(n)); } else { (void)const_cast<oop&>(v =*(volatile oop*) addr); } OrderAccess::acquire(); return JNIHandles::make_local(env, v); UNSAFE_END ![]()
UNSAFE_ENTRY(jobject,Unsafe_GetObjectVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlongoffset)) UnsafeWrapper("Unsafe_GetObjectVolatile"); oop p = JNIHandles::resolve(obj); void* addr =index_oop_from_field_offset_long(p, offset); volatile oop v; if (UseCompressedOops) { volatile narrowOop n = *(volatilenarrowOop*) addr; (void)const_cast<oop&>(v =oopDesc::decode_heap_oop(n)); } else { (void)const_cast<oop&>(v =*(volatile oop*) addr); } OrderAccess::acquire(); return JNIHandles::make_local(env, v);UNSAFE_END我們來看看到代碼volatile oopv; 定義了一個c++的volatile,進行一個讀取操作;c++ volatile具有的語義是每次都能夠從內(nèi)存中看到的最新的,oop這個類是java中的對象類在c++中對應(yīng)的實現(xiàn)類,即使用c++的語義讀取了這個對象的引用地址,保證了可見性.而后是acquire指令.
OrderAccess::acquire();
從上文可知acquire是有l(wèi)oadload,和loadStore組成的這就是volatile變量所要插入的柵欄,并且可以保證,acquire之后的讀都是具有可見性的,x86_64在jdk9中是有編譯器柵欄實現(xiàn)的,可以推斷出,任意的一個變量通過getObjectVolatile是可以獲取javavolatile的讀的語義的.
找到putObjectVolatile在unsafe.cpp中的具體實現(xiàn),
[cpp] view plain copyprint?
UNSAFE_ENTRY(void,Unsafe_SetObjectVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlongoffset, jobject x_h)) UnsafeWrapper("Unsafe_SetObjectVolatile"); oop x = JNIHandles::resolve(x_h); oop p = JNIHandles::resolve(obj); void* addr =index_oop_from_field_offset_long(p, offset); OrderAccess::release(); if (UseCompressedOops) { oop_store((narrowOop*)addr, x); } else { oop_store((oop*)addr, x); } OrderAccess::fence(); UNSAFE_END ![]()
UNSAFE_ENTRY(void,Unsafe_SetObjectVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlongoffset, jobject x_h)) UnsafeWrapper("Unsafe_SetObjectVolatile"); oop x = JNIHandles::resolve(x_h); oop p = JNIHandles::resolve(obj); void* addr =index_oop_from_field_offset_long(p, offset); OrderAccess::release(); if (UseCompressedOops) { oop_store((narrowOop*)addr, x); } else { oop_store((oop*)addr, x); } OrderAccess::fence();UNSAFE_ENDopp_store是JavaObject變量的寫,在這之前插入了release,在之后插入了fence,這邊又有了javavolatile寫的語義,因為release對應(yīng)的loadStore,storestore的兩個柵欄;fence對應(yīng)的是storeload柵欄,這與java volatile的寫所需要的柵欄是一致的.
總結(jié):一個引用要變成volatile的讀的語義,只要用getObjectVolatile,一個引用要想有volatile寫的語義,主要putObjectvolatile.
5.2 volatile的相關(guān)特性
5.2 volatile的寫
從上文可知道,volatile的寫所插入的storeLoad是一個耗時的操作,所以出現(xiàn)了一個對volatile寫的升級版本.
AtomicIntegeri=new AtomicInteger(5);
i.lazySet(5);
如上利用laySet方法進行性能優(yōu)化,在實現(xiàn)上對volatile的寫只會在之前插入storestore柵欄,這樣可以保證的volatile寫之前的寫都不能重排到volatile寫之后,volatile之前的寫都會在volatile變量寫之前刷入主存,但是這個方法雖然提高了效率,但是volatile的寫并不能被馬上被其它線程看到,通常需要幾納秒才能被其它線程看到,這個時間比較短,所以代價是可以忍受的.
5.2.2 volatile的讀
網(wǎng)上流傳著volatile的讀與普通變量是差不多的性能,但是如果你讀了本文,你就知道,volatile的讀其實也是有代價,而且相比普通的變量的讀要慢的多.原因是,volatile的變量不僅要從內(nèi)存中讀,還使得它之后的所讀的變量也是從內(nèi)存中讀的.
為此常常將循環(huán)中對volatile的變量的讀,提取到循環(huán)之外.
Volatile int I= 0;
For(int j = 0;j < 100000;j++){
Int p = I;
}
優(yōu)化為
Int p = I;
For(int j = 0;j < 100000;j++){
//直接操作p
}
如上,這樣就可以使得volatile變量緩存在寄存器中,那么就不用每次都去從主存中去讀取,這樣就可以大大的提高性能.
5.2.3 volatile與原子性
Volatile標(biāo)識的變量的純粹的讀和寫都是原子,普通long變量,double變量的讀寫是不具有原子性的,但是對volatile的i++,i—操作都不是原子操作,這是volatile的不足.
Javap -c Test2
[cpp] view plain copyprint?
public class Test2 { private staticvolatile int i=0; i++; public staticvoid main(java.lang.String[]); Code: 0: getstatic #10// Field i:I 3: iconst_1 4: iadd 5: putstatic #10// Field i:I 8: return ![]()
public class Test2{private staticvolatile int i=0;i++;public staticvoid main(java.lang.String[]); Code:0: getstatic #10// Field i:I3: iconst_14: iadd5: putstatic #10// Field i:I8: return
如上面代碼所示:i++是由4個操作組合而成,volatile的加一操作并沒有保證原子操作,所以如果有多個線程對volatile進行操作,需要加鎖,或者使用Atomic類.
轉(zhuǎn)載請注明來源 http://blog.csdn.NET/w329636271/article/details/54616543 老師:leader 合作者<wangzhancheng.com@QQ.com>
變量,double變量的讀寫是不具有原子性的,但是對volatile的i++,i—操作都不是原子操作,這是volatile的不足.
Javap -c Test2
[cpp] view plain copyprint?
public class Test2 { private staticvolatile int i=0; i++; public staticvoid main(java.lang.String[]); Code: 0: getstatic #10// Field i:I 3: iconst_1 4: iadd 5: putstatic #10// Field i:I 8: return ![]()
public class Test2{private staticvolatile int i=0;i++;public staticvoid main(java.lang.String[]); Code:0: getstatic #10// Field i:I3: iconst_14: iadd5: putstatic #10// Field i:I8: return
如上面代碼所示:i++是由4個操作組合而成,volatile的加一操作并沒有保證原子操作,所以如果有多個線程對volatile進行操作,需要加鎖,或者使用Atomic類.
轉(zhuǎn)載請注明來源 http://blog.csdn.NET/w329636271/article/details/54616543 老師:leader 合作者<wangzhancheng.com@qq.com>
新聞熱點
疑難解答



















