(1)多核并發(fā)緩存架構(gòu)
早期計算機先把數(shù)據(jù)(硬盤數(shù)據(jù))加載到主內(nèi)存左敌,然后CPU再到內(nèi)存中取。由于現(xiàn)在CPU發(fā)展很快,CPU的運算速度比主內(nèi)存高得多瓦宜,為了避免受主內(nèi)存讀取速度的影響,所以現(xiàn)在會在CPU中有CPU緩存岭妖,速度接近CPU临庇,比主內(nèi)存快得多反璃,只要數(shù)據(jù)在CPU緩存,那么CPU的速度就沒有太大的限制假夺,發(fā)揮到最大
L1淮蜈、L2、L3就是CPU的高速緩存
(2)JMM內(nèi)存模型
Java多線程內(nèi)存模型跟CPU緩存模型類似已卷,是基于cpu緩存模型來建立的梧田,Java線程內(nèi)存模型是標準化的,屏蔽掉了低層不同計算機的區(qū)別侧蘸。
假設主內(nèi)存中有共享變量 int a=6裁眯,線程1把它改為7,線程2是未必能看到最新值的讳癌,因為線程和主內(nèi)存之間還有一個工作內(nèi)存穿稳,存儲這共享變量副本,線程1會先把工作內(nèi)存的值改為7析桥,在刷到主內(nèi)存中司草,但線程2的工作內(nèi)存值還是6,未必感知得到
證明:
public class VolatileVisibilityTest {
private static boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("waiting data...");
while(!initFlag){
}
System.out.println("===============success");
}).start();
Thread.sleep(2000);
new Thread(() -> prepareData()).start();
}
public static void prepareData(){
System.out.println("prepare data...");
initFlag = true;
System.out.println("prepare data end...");
}
}
在initFlag變?yōu)榱藅rue之后泡仗,但線程1還沒感知得到埋虹,仍處于死循環(huán)中
(3)volatile
只要使用了volatile修飾,就能馬上知道最新值娩怎,退出while循環(huán)
private static volatile boolean initFlag = false;
(4)volatile怎么保證線程可見性
內(nèi)存模型原子操作:
read(讀壬巍):從主內(nèi)存讀取數(shù)據(jù)
load(載入):將主內(nèi)存讀取到的數(shù)據(jù)寫入工作內(nèi)存
use(使用):從工作內(nèi)存讀取數(shù)據(jù)來計算
assign(賦值):將計算好的值重新賦值到工作內(nèi)存中
store(存儲):將工作內(nèi)存數(shù)據(jù)寫入主內(nèi)存
write(寫入):將store過去的變量值賦值給主內(nèi)存中的變量
lock(鎖定):將主內(nèi)存變量加鎖,標識為線程獨占狀態(tài)
unlock(解鎖):將主內(nèi)存變量解鎖截亦,解鎖后其他線程可以鎖定該變量
下面先慢慢展開
(5)緩存一致性協(xié)議(MESI)
多個cpu從主內(nèi)存讀取同一個數(shù)據(jù)到各自的高速緩存爬泥,當其中某個cpu修改了緩存里的數(shù)據(jù),該數(shù)據(jù)會馬上同步回主內(nèi)存崩瓤,其他cpu通過總線嗅探機制
可以感知到數(shù)據(jù)的變化從而將自己緩存里的數(shù)據(jù)失效
(6)查看volatile匯編指令
首先下載hsdis-amd64.dll
袍啡,然后復制到如C:\Program Files\Java\jdk1.8.0_261\jre\bin
中,然后在運行java程序的時候却桶,要先配置一些jvm參數(shù)
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileVisibilityTest.prepareData
如果不同類名方法名的還得改改這里的內(nèi)容
如果提示了
PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
境输,說明是PrintAssembly功能以及開啟,但系統(tǒng)不支持颖系,把hsdis-amd64.dll
復制到如C:\Program Files\Java\jdk1.8.0_261\jre\bin\server
目錄吧嗅剖,親測可行,以下就是輸出的匯編信息其中*putstatic initFlag是JVM指令碼嘁扼,作用是給靜態(tài)變量賦值信粮。lock、mov這些都是匯編語言趁啸。
沒有volatile修飾的時候强缘,沒有l(wèi)ock指令督惰,volatile修飾的變量在賦值的時候會有l(wèi)ock。
(7)volatile緩存可見性實現(xiàn)原理
底層實現(xiàn)主要是通過匯編lock前綴指令欺旧,它會鎖定這塊內(nèi)存區(qū)域的緩存(緩存行鎖定)并回寫到主內(nèi)存
IA-32和Intel 64架構(gòu)軟件開發(fā)者手冊對lock指令的解析:
Ⅰ姑丑、會將當前處理器緩存行的數(shù)據(jù)立即寫回到系統(tǒng)內(nèi)存
Ⅱ、這個寫回內(nèi)存的操作會引起在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效(MESI協(xié)議)
Ⅲ辞友、提供內(nèi)存屏障功能,使lock前后指令不能重排序
其中第一點中震肮,lock能使變量賦值完后立即執(zhí)行store和write称龙,沒有l(wèi)ock的話是不會立即執(zhí)行的,會不知道什么時候執(zhí)行戳晌。所有l(wèi)ock會二話不說把數(shù)據(jù)刷回主內(nèi)存中鲫尊。立即刷回到主內(nèi)存的目的是讓多線程能及時感知到修改,強調(diào)及時性沦偎。
第二點疫向,MESI,指定的是
M狀態(tài)(修改)
E狀態(tài)(獨享)
S狀態(tài)(共享)
I狀態(tài)(無效)
把變量改為Invalid狀態(tài)豪嚎,數(shù)據(jù)就無效了搔驼。MESI內(nèi)容比較多,可以自己搜索一下侈询。
第三點后面再說舌涨。
假如面試提到volatile的原理,大概吹一下是通過匯編lock前綴指令扔字、然后這個指令的行為(立即囊嘉、MESI)就差不多了
(8)指令重排序與內(nèi)存屏障
并發(fā)編程三大特寫:可見性、有序性革为、原子性
volatile保證可見性與有序性扭粱,但是不保證原子性,保證原子性需要借助synchronized這樣的鎖機制
-
指令重排序:在不影響單線程程序執(zhí)行的結(jié)果的前提下震檩,計算機為了最大限度的發(fā)揮機器性能琢蛤,會對機器指令重排序優(yōu)化
重排序會遵循as-if-serial與happens-before原則
阿里面試題:雙重檢測鎖DCL對象半初始化問題
(9)什么是有序性
程序在執(zhí)行的時候是有順序行的,下面看一個例子恳蹲,猜一下可能會輸出什么
public class VolatileSerialTest {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Set<String> resultSet = new HashSet<>();
for (int i = 0; i < 10000000; i++) {
x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(() -> {
a = y;
x = 1;
});
Thread other = new Thread(() -> {
b = x;
y = 1;
});
one.start();
other.start();
one.join();
other.join();
resultSet.add("a=" + a + ",b=" + b);
System.out.println(resultSet);
}
}
}
就這么一看虐块,ab的組合可能有00、01嘉蕾、10贺奠,但不會有11。
但實際上错忱,隨著程序的運行儡率,是會出現(xiàn)11的情況挂据,也就是a=1,也就是y=1儿普,也就是要先other線程先執(zhí)行完成崎逃,這樣b應該是0才對,但為什么會是1眉孩,這個就是指令重排序的影響瞒斩。
在最終指令執(zhí)行之前,可能會出現(xiàn)幾種情況的重排序共苛,如編譯器優(yōu)化重排序雌贱、指令級并行重排序、內(nèi)存系統(tǒng)重排序死遭,所以出現(xiàn)11的情況可能是執(zhí)行了
x=1; => y=1; => a=y; => b=x;
為什么會自作主張重排序广恢,大概是cpu認為當前指令比較耗時,而后面的指令結(jié)果會在其他地方使用呀潭,就先執(zhí)行了钉迷,讓其他地方可以不用等這么久,然后再執(zhí)行那個耗時的操作钠署。就是編譯器和處理器為了提高并行度糠聪。
這就出bug了,但不是計算機的bug踏幻,是你代碼的bug枷颊,是你自己沒考慮到11的情況,而不是理所當然的不可能有11该面,這就是并發(fā)的難點夭苗。
(10)重排序原則
操作系統(tǒng)不會亂排序,而是有依據(jù)的隔缀,會遵循as-if-serial與happens-before原則题造。
as-if-serial:不管怎么重排序,(單線程)程序的執(zhí)行結(jié)果不能被改變猾瘸。
假如有a=y和x=a界赔,這個就是不能重排序的,因為有依賴關系牵触,重排序的話x的值是變化的
像之前的程序淮悼,a=y和x=1,并沒有任何關系揽思,重排序并不影響結(jié)果袜腥,所以是允許重排序的,它不管是不是影響了其他線程钉汗,as-if-serial只管單線程
假設oracle的JDK產(chǎn)品經(jīng)理要求“a=y和x=a”的時候不能重排序羹令,“a=y和x=1”的時候可以重排序鲤屡,那么開發(fā)人員如何實現(xiàn)JDK這個軟件?
編譯原理會進行語義分析生成語義樹福侈,判斷代碼之間有沒有依賴關系酒来,有依賴就不能重排序
happens-before:
①程序順序原則:即在一個線程內(nèi)必須保證語義串行性,也就是說按照代碼順序執(zhí)行肪凛。
②鎖規(guī)則:解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個鎖的加鎖(lock)之前堰汉,也就是說,如果對于一個鎖解鎖后伟墙,再加鎖衡奥,那么加鎖的動作必須在解鎖動作之后(同一個鎖)。
obj.lock();
obj.unlock();
obj.lock();
obj.unlock();
就是第2第3行不能重排序
③volatile規(guī)則:volatile變量的寫远荠,先發(fā)生于讀,這保證了volatile變量的可見性失息,簡單的理解就是譬淳,volatile變量在每次被線程訪問時,都強迫從主內(nèi)存中讀該變量的值盹兢,而當該變量發(fā)生變化時邻梆,又會強迫將最新的值刷新到主內(nèi)存,任何時刻绎秒,不同的線程總是能夠看到該變量的最新值浦妄。
④線程啟動規(guī)則:線程的start()方法先于它的每一個動作,即如果線程A在執(zhí)行線程B的start方法之前修改了共享變量的值见芹,那么當線程B執(zhí)行start方法時剂娄,線程A對共享變量的修改對線程B可見。
⑤傳遞性:A先于B玄呛,B先于C阅懦,那么A必然先于C
⑥線程終止規(guī)則:線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待當前執(zhí)行的線程終止徘铝。假設在線程B終止之前耳胎,修改了共享變量,線程A從線程B的join方法成功放回后惕它,線程B對共享變量的修改將對線程A可見怕午。
⑦線程終端規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測線程是否中斷淹魄。
⑧對象終結(jié)規(guī)則:對象的構(gòu)造函數(shù)執(zhí)行郁惜,結(jié)束先于finalize()方法。
(11)阿里面試題:雙重檢測鎖DCL對象半初始化問題
在阿里巴巴手冊中揭北,有一個推薦做法
早期的時候扳炬,雙重檢測鎖單例模式就是這么寫的吏颖,主要是解決并發(fā)的問題,避免重復初始化
public class DoubleCheckLockSingleton {
private static DoubleCheckLockSingleton instance = null;
private DoubleCheckLockSingleton(){}
//雙重檢測鎖單例
public static DoubleCheckLockSingleton getInstance(){
if(instance == null){ //一重檢測
synchronized (DoubleCheckLockSingleton.class){ //鎖
if(instance == null){ //二重檢測
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
public static void main(String[] args) {
DoubleCheckLockSingleton instance = DoubleCheckLockSingleton.getInstance();
}
}
但它存在點問題恨樟,我們先在idea中安裝jclasslib插件半醉,看看這個類的指令碼
其中synchronized對應的就是monitorenter至monitorexit
10 monitorenter
獲取靜態(tài)變量
11 getstatic #2 <tuling/DoubleCheckLockSingleton.instance>
判斷是不是為null
14 ifnonnull 27 (+13)
是的話就new
17 new #3 <tuling/DoubleCheckLockSingleton>
20 dup
執(zhí)行init方法(下面簡單說明一下,和這部分內(nèi)容無關)
21 invokespecial #4 <tuling/DoubleCheckLockSingleton.<init>>
給變量賦值劝术,即instance=xx
24 putstatic #2 <tuling/DoubleCheckLockSingleton.instance>
27 aload_0
28 monitorexit
對象創(chuàng)建的主要流程
執(zhí)行<init>方法缩多,即對象按照程序員的意愿進行初始化。對應到語言層面上講养晋,就是為屬性賦值(主要衬吆,這與上面的賦零值不同,這是由程序員賦的值)绳泉,和執(zhí)行構(gòu)造方法逊抡。
回到問題上,究竟是會出現(xiàn)什么問題零酪?就是 invokespecial 和 putstatic 有可能重排序冒嫡,因為沒有違背as-if-serial與happens-before原則。
對于單線程下四苇,重排序的結(jié)果并沒有影響
重排序之后:先把17行的地址給24行孝凌,instance已經(jīng)不為空了,但初始化是還沒完成的月腋,未執(zhí)行完init都是未完成蟀架,這時是對象的半初始化。當其他線程拿到了這個還沒完成初始化的變量時榆骚,就會出現(xiàn)問題片拍。(這種問題只有極端情況才會偶爾出現(xiàn))
所以要加volatile,就不會有重排序寨躁。
(12)內(nèi)存屏障
volatile底層會幫我們實現(xiàn)內(nèi)存屏障穆碎。如何理解內(nèi)存屏障?
如a=y; x=1; 如果不想這兩行代碼重排序职恳,在它們中間加一行標記性代碼所禀,跟重排序(CPU )做好約定,當遇到這個標記的時候就不能對它前后的代碼進行重排序放钦,那么這個標記性代碼就叫做內(nèi)存屏障
- JVM規(guī)范定義的內(nèi)存屏障
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保證load1的讀取操作在load2及后續(xù)讀取操作之前執(zhí)行 |
StoreStore | Store; StoreStore; Store2 | 在store2及其后的寫操作執(zhí)行前色徘,保證store1的寫操作已刷新到主內(nèi)存 |
LoadStore | Load1; LoadStore; Store2 | 在Store2及其后的寫操作執(zhí)行前,保證load1的讀操作已讀取結(jié)束 |
StoreLoad | Store1; StoreLoad; Load2 | 保證store1的寫操作已刷新到主內(nèi)存之后操禀,load2及其后的讀操作才能執(zhí)行 |
這些屏障是jdk規(guī)定的褂策,就是jdk的程序員實現(xiàn)的,load就是加載,store就是寫
LoadLoad:如b=a; c=a; 對應a來說斤寂,都是加載耿焊,只有它們之間加了LoadLoad,就不允許重排序遍搞。
- JVM規(guī)定volatile需要實現(xiàn)的內(nèi)存屏障
a=2; //volatile寫罗侯,a為volatile變量
StoreStore屏障
a=1; //volatile寫
StoreLoad屏障
b=a; //volatile讀
LoadLoad屏障
LoadStore屏障
也就是jdk的程序員在實現(xiàn)volatile的時候,就是在對它的操作前后都要加屏障溪猿,具體實現(xiàn)的話看看源碼 openjdk - https://github.com/openjdk/jdk/blob/master/src/hotspot/share/interpreter/zero/bytecodeInterpreter.cpp
這個switch就是對不同類型的volatile進行各自的操作钩杰,最后都會執(zhí)行一個 OrderAccess::storeload();,可以看看這個鏈接orderAccess_linux_x86.hpp
前面asm就是調(diào)用匯編語言的意思诊县,后面的就是匯編語言讲弄,還記得這個匯編語言嗎?回看上方第6點提到的
沒有volatile修飾的時候依痊,沒有l(wèi)ock指令避除,volatile修飾的變量在賦值的時候會有l(wèi)ock。
lock前綴指令:在上方第7點中也提到
提供內(nèi)存屏障功能胸嘁,使lock前后指令不能重排序
驹饺,當很多的硬件看到這個lock前綴指令,就不會對前后左右的代碼進行重排序缴渊,就是一個約定好的代碼指令,看到就不能重排序鱼炒。
- 不同CPU硬件對于JVM的內(nèi)存屏障規(guī)范實現(xiàn)指令不一樣
- Intel CPU硬件級內(nèi)存屏障實現(xiàn)指令
- lfence:是一種Load Barrier讀屏障衔沼,實現(xiàn)LoadLoad屏障
- sfence:是一種Store Barrier寫屏障,實現(xiàn)StoreStore屏障
- mfence: 是一種全能型的屏障昔瞧,具備lfence和sfence的能力指蚁,具有所有屏障能力
- JVM低層簡化了內(nèi)存屏障硬件指令的實現(xiàn)
- lock前綴:lock指令不是一種內(nèi)存屏障,但是它能完成類似內(nèi)存屏障的功能
(13)從Spring Cloud微服務框架源碼看下并發(fā)編程的應用
從github下載nacos源碼自晰,后續(xù)再看