計算機多核并發(fā)緩存架構(gòu)
下圖是計算機運行架構(gòu)圖:
??由于cpu的運行程序速度遠大于主存儲的速度裳食,所以會在主存RAM和CPU之間加多級高速緩存矛市,緩存的速度接近cpu的運行速度,木桶效應诲祸,水能裝多少浊吏,取決于短板而昨,因此這樣會大大提高計算機的運行速度。
java內(nèi)存模型
定義(摘抄《深入理解+Java+內(nèi)存模型_程曉明》)
??java線程內(nèi)存模型跟cpu緩存模型類似找田,是基于cpu緩存模型來建立的歌憨,java線程內(nèi)存模型是標準化的。
在java中墩衙,所有實例域务嫡,靜態(tài)域和數(shù)組元素存儲在堆內(nèi)存中,堆內(nèi)存在線程之間共享漆改。
??局部變量 心铃,方法定義參數(shù)和異常處理器參數(shù),這些不會在線程之間 共享籽懦,它們不會有內(nèi)存可見性問題于个,也不受內(nèi)存模型的影響
??Java線程之間的通信由Java內(nèi)存模型(本文簡稱為JMM)控制,JMM決定一個 線程對共享變量的寫入何時對另一個線程可見暮顺。從抽象的角度來看厅篓,JMM定義了 線程和主內(nèi)存之間的抽象關系:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(local memory)捶码,本地內(nèi)存 中存儲了該線程以讀/寫共享變量的副本羽氮。本地內(nèi)存是JMM的一個抽象概念,并不 真實存在惫恼。它涵蓋了緩存档押,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化祈纯。Java 內(nèi)存模型的抽象示意圖如下:
以上還是比較通俗易懂的
內(nèi)存模型的三大特性
可見性:
可見性是一種復雜的屬性令宿,因為可見性中的錯誤總是會違背我們的直覺。通常腕窥,我們無法確保執(zhí)行讀操作的線程能適時地看到其他線程寫入的值粒没,有時甚至是根本不可能的事情。為了確保多個線程之間對內(nèi)存寫入操作的可見性簇爆,必須使用同步機制癞松。
可見性,是指線程之間的可見性入蛆,一個線程修改的狀態(tài)對另一個線程是可見的响蓉。也就是一個線程修改的結(jié)果。另一個線程馬上就能看到哨毁。比如:用volatile修飾的變量枫甲,就會具有可見性。volatile修飾的變量不允許線程內(nèi)部緩存和重排序,即直接修改內(nèi)存言秸。所以對其他線程是可見的软能。但是這里需要注意一個問題蛔溃,volatile只能讓被他修飾內(nèi)容具有可見性堵腹,但不能保證它具有原子性和簸。比如 volatile int a = 0;之后有一個操作 a++抄沮;這個變量a具有可見性,但是a++ 依然是一個非原子操作岖瑰,也就是這個操作同樣存在線程安全問題叛买。
在 Java 中 volatile、synchronized 和 final 實現(xiàn)可見性蹋订。
原子性:
原子是世界上的最小單位率挣,具有不可分割性。比如 a=0露戒;(a非long和double類型) 這個操作是不可分割的椒功,那么我們說這個操作時原子操作。再比如:a++智什; 這個操作實際是a = a + 1动漾;是可分割的,所以他不是一個原子操作荠锭。非原子操作都會存在線程安全問題旱眯,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作证九,那么我們稱它具有原子性删豺。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法愧怜。比如:AtomicInteger呀页、AtomicLong、AtomicReference等叫搁。
在 Java 中 synchronized 和在 lock赔桌、unlock 中操作保證原子性。
有序性:
Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證線程之間操作的有序性渴逻,volatile 是因為其本身包含“禁止指令重排序”的語義疾党,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規(guī)則獲得的,此規(guī)則決定了持有同一個對象鎖的兩個同步塊只能串行執(zhí)行惨奕。
volatile一致性
看一下下面代碼:
package com.zhaoyan.volatileexample;
public class VolatileVisibleness {
private static boolean flag =false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("等待數(shù)據(jù)準備中---");
while(!flag){
}
System.out.println("數(shù)據(jù)準備結(jié)束雪位,開始執(zhí)行...");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
setFlag();
}
}).start();
}
public static void setFlag() {
System.out.println("數(shù)據(jù)準備中---");
VolatileVisibleness.flag = true;
System.out.println("數(shù)據(jù)準備結(jié)束---");
}
}
以上代碼分析:
??主函數(shù)中有兩個線程,一個線程用來獲取邏輯處理標簽flag結(jié)果標簽梨撞,并繼續(xù)執(zhí)行程序雹洗,一個線程用來處理flag標簽邏輯香罐。我們想要的結(jié)果是,在線程休眠2秒之后时肿,flag=true的時候庇茫,被第一個線程獲取到,并繼續(xù)執(zhí)行 程序螃成,打印數(shù)據(jù)準備結(jié)束旦签,開始執(zhí)行...
執(zhí)行結(jié)果如下:
如圖可知,程序在死循環(huán)等待中寸宏,也就是說
這行代碼執(zhí)行之后宁炫,while的條件沒有被執(zhí)行,數(shù)據(jù)準備結(jié)束氮凝,開始執(zhí)行...未被打痈岢病;
這個現(xiàn)象驗證了內(nèi)存模型中存在這樣的一塊區(qū)域是線程獨享的罩阵。
??當我修改線程B中的局部變量后竿秆,線程A并不知道flag的值已經(jīng)被修改,導致程序死循環(huán)永脓。
如何達到預期的效果袍辞,使數(shù)據(jù)準備結(jié)束,開始執(zhí)行...開始執(zhí)行呢常摧!就是加volatile關鍵字修改變量搅吁。
private static volatile boolean flag =false;
然后在執(zhí)行的結(jié)果如下:
等待數(shù)據(jù)準備中---
數(shù)據(jù)準備中---
數(shù)據(jù)準備結(jié)束---
數(shù)據(jù)準備結(jié)束,開始執(zhí)行...
內(nèi)存之間的原子操作
Java 內(nèi)存模型對主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議定義了八種操作落午,具體如下:
read(讀然雅场):作用于主內(nèi)存變量,把一個變量從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中溃斋,以便隨后的 load 動作使用界拦。
load(載入):作用于工作內(nèi)存變量,把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中梗劫。
use(使用):作用于工作內(nèi)存變量享甸,把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要使用變量值的字節(jié)碼指令時執(zhí)行此操作梳侨。
assign(賦值):作用于工作內(nèi)存變量蛉威,把一個從執(zhí)行引擎接收的值賦值給工作內(nèi)存的變量,每當虛擬機遇到一個需要給變量進行賦值的字節(jié)碼指令時執(zhí)行此操作走哺。
store(存儲):作用于工作內(nèi)存變量蚯嫌,把工作內(nèi)存中一個變量的值傳遞到主內(nèi)存中,以便后續(xù) write 操作。
write(寫入):作用于主內(nèi)存變量择示,把 store 操作從工作內(nèi)存中得到的值放入主內(nèi)存變量中束凑。
lock(鎖定):作用于主內(nèi)存變量,把一個變量標識為一條線程獨占狀態(tài)栅盲。
unlock(解鎖):作用于主內(nèi)存變量汪诉,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定剪菱。
普通變量底層執(zhí)行流程
根據(jù)前面代碼摩瞎,我們根據(jù)這個圖來說明一下程序的執(zhí)行流程:
volatile修飾后的變量底層執(zhí)行流程
如何讓工作內(nèi)存的變量相等!P⒊!!
早期解決變量一致性的問題設計:
總線加鎖:
??cpu從主內(nèi)存讀取數(shù)據(jù)到高速緩存蚓哩,會在總線對這個數(shù)據(jù)加鎖构灸,這樣其他cpu沒法去讀或?qū)戇@個數(shù)據(jù),直到cpu使用完數(shù)據(jù)釋放鎖之后岸梨,其他cpu才能讀取該數(shù)據(jù)喜颁。
??在線程B讀數(shù)據(jù)到工作內(nèi)存中的時候進行加鎖,一直到cpu執(zhí)行結(jié)束并將數(shù)據(jù)重新寫到對內(nèi)存之后才能釋放鎖曹阔,這個時候線程A再讀取已經(jīng)改變的數(shù)據(jù)加載到線程A的本地內(nèi)存中操作半开。
??這樣的做存在的問題有:
(1)性能降低
(2)沒有充分利用多核計算機的特性
緩存一致性協(xié)議:
??多個cpu從主內(nèi)存讀取同一個數(shù)據(jù)到各自的高速緩存,當其中某個cpu修改了緩存里的數(shù)據(jù)赃份,該數(shù)據(jù)會馬上同步回主內(nèi)存寂拆,其他cpu通過總線嗅探機制(監(jiān)聽)可以感知數(shù)據(jù)的變化從而將自己的緩存里的數(shù)據(jù)失效。
??對比總線加鎖抓韩,緩存一致性協(xié)議的鎖的力度小了很多纠永,只在總線寫入前開始加鎖,為了保證總線監(jiān)聽到的數(shù)據(jù)一定是被寫入了堆內(nèi)存谒拴〕⒔看volatile的執(zhí)行匯編底層語言,也會看到在數(shù)據(jù)修改前會加lock英上。
??以上是volatile的可見性炭序。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile 變量最后的寫入苍日;
volatile原子性
??在JSR -133之前的舊內(nèi)存模型中惭聂,一個64位long/ double型變量的讀/ 寫操作可以被拆分為兩個32位的讀/寫操作來執(zhí)行。從JSR -133內(nèi)存模型開始 (即從JDK5開始)易遣,僅僅只允許把一個64位long/ double型變量的寫操作拆分 為兩個32位的寫操作來執(zhí)行彼妻,任意的讀操作在JSR -133中都必須具有原子性(即 任意讀操作必須要在單個讀事務中執(zhí)行)
??對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這 種復合操作不具有原子性。
??鎖的happens-before規(guī)則保證釋放鎖和獲取鎖的兩個線程之間的內(nèi)存可見性侨歉,這 意味著對一個volatile變量的讀屋摇,總是能看到(任意線程)對這個volatile變量最 后的寫入。
看一下下面代碼:
package com.zhaoyan.volatileexample;
public class VolatileAtomic {
private static volatile long num = 0L;
private static void increase() {
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
Thread thread = threads[i];
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
increase();
}
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
}
以上代碼分析如下:
??十個線程一起執(zhí)行num+1的操作幽邓,等十個線程都執(zhí)行結(jié)束之后炮温,將num的數(shù)值輸出,num用volatile修飾牵舵,保證內(nèi)存的可見性柒啤;
代碼執(zhí)行結(jié)果如下:
可以看到,執(zhí)行的結(jié)果是10000以下畸颅;
為什么會出現(xiàn)這種情況担巩?
結(jié)合圖例:
分析:
??線程A加載num之后,use没炒,進行num++涛癌,之后assign,num=1送火,在num store總線加鎖之前拳话,線程B執(zhí)行,num的assign种吸,num=1弃衍;這個時候,線程A進行l(wèi)ock坚俗,根據(jù)總線嗅探機制镜盯,這個時候線程B發(fā)現(xiàn)數(shù)據(jù)有變化,進行變量失效坦冠,導致形耗,線程B執(zhí)行的mum++的結(jié)果值失效,進而形成程序執(zhí)行的效果辙浑;
??鎖的語義決定了臨界區(qū)代碼的執(zhí)行具有原子性激涤。這意味著即使是64位的long型和 double型變量,只要它是volatile變量判呕,對該變量的讀寫就將具有原子性倦踢。如果 是多個volatile操作或類似于volatile++這種復合操作,這些操作整體上不具有原 子性侠草。
以上我們驗證了volatile的原子性辱挥;
volatile有序性
??在執(zhí)行程序時為了提高性能,編譯器和處理器常常會對指令做重排序边涕。
??為了實現(xiàn)volatile的內(nèi)存語義晤碘,編譯器在生成字節(jié)碼時褂微,會在指令序列中插入內(nèi)存 屏障來禁止特定類型的處理器重排序。
??代碼樣例:
package com.zhaoyan.volatileexample;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName VolatileOrder
* @Description TODO
* @Author zhaoyan
* @Date 2019/7/31 11:03
* @Version 1.0
**/
public class VolatileOrder {
static int x = 0, y = 0;
public static void main(String[] args) throws Exception {
Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
x = 0;
y = 0;
map.clear();
Thread one = new Thread(new Runnable() {
@Override
public void run() {
int a = y; //1
x = 1; //2
map.put("a", a);
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
int b = x; //3
y = 1; //4
map.put("b", b);
}
});
one.start();
two.start();
one.join();
two.join();
System.out.println(map.toString());
}
}
}
以上代碼分析:
正常理解的情況下园爷,我們能推測出程序答應的結(jié)果可能是
(1)線程1先執(zhí)行宠蚂,然后再執(zhí)行線程2,這樣輸出的結(jié)果是[0,1]童社;
(2)線程2先執(zhí)行求厕,然后再執(zhí)行線程1,這樣輸出的結(jié)果是[1,0]扰楼;
(3)線程1先執(zhí)行呀癣,但是沒有執(zhí)行到x=1的時候,線程2 執(zhí)行弦赖,結(jié)果是[0,0]项栏;
實際結(jié)果如下:
??我們看到執(zhí)行結(jié)果有4種情況,那么[1,1]的情況是怎么出現(xiàn)的呢蹬竖?
??這里面涉及到JMM模型的重排序原理忘嫉,主要有if-else-serial語義,數(shù)據(jù)依賴性案腺,程序順序規(guī)則等相關知識。不一一說明了康吵;
volatile修飾的變量可以阻止這種重排序劈榨,利用的內(nèi)存屏障的原理;
??volatile的內(nèi)存語義:嚴格限制編譯器和處理器對volatile變量與普通變量的重排 序晦嵌,確保volatile的寫-讀和鎖的釋放-獲取具有相同的內(nèi)存語義同辣。從編譯器重排序 規(guī)則和處理器內(nèi)存屏障插入策略來看,只要volatile變量與普通變量之間的重排序 可能會破壞volatile的內(nèi)存語義惭载,這種重排序就會被編譯器重排序規(guī)則和處理器內(nèi) 存屏障插入策略禁止旱函。
以上說明了volatile在并發(fā)三大特性中所發(fā)揮的作用,如果覺得可以描滔,請留下愛心棒妨;