前言
1、并發(fā)編程三要素
????在并發(fā)編程的世界里,下面三要素你必須清楚:
- 可見(jiàn)性:可見(jiàn)性指多個(gè)線程操作一個(gè)共享變量時(shí),其中一個(gè)線程對(duì)變量進(jìn)行修改后,其他的線程可以立即看到修改的結(jié)果。
- 原子性:原子性指的是一個(gè)或多個(gè)操作,要么全部執(zhí)行,并且執(zhí)行過(guò)程中不被其它操作打斷,要么就全部不執(zhí)行。
- 有序性:程序的執(zhí)行順序按照代碼的先后順序來(lái)執(zhí)行。
2、并發(fā)編程的三大問(wèn)題
????我們知道CPU、內(nèi)存、IO三者的速度存在很大的差異,為了平衡三者之間的速度差異,做了如下改變:
- CPU 增加了緩存,以均衡與內(nèi)存的速度差異;
- 操作系統(tǒng)增加了進(jìn)程、線程,以分時(shí)復(fù)用 CPU,進(jìn)而均衡 CPU 與 I/O 設(shè)備的速度差異;
- 編譯程序優(yōu)化指令執(zhí)行次序,使得緩存能夠得到更加合理地利用。
????為了使處理器內(nèi)部運(yùn)算單元盡可能被充分利用,處理器還會(huì)對(duì)輸入的代碼進(jìn)行亂序執(zhí)行(Out-Of-Order Execution)優(yōu)化,處理器會(huì)在亂序執(zhí)行之后的結(jié)果進(jìn)行重組,保證結(jié)果的正確性,也就是保證結(jié)果與順序執(zhí)行的結(jié)果一致。但是在真正的執(zhí)行過(guò)程中,代碼執(zhí)行的順序并不一定按照代碼的書(shū)寫(xiě)順序來(lái)執(zhí)行,可能和代碼的書(shū)寫(xiě)順序不同。
????但這些改變可能導(dǎo)致詭異性的bug:
- 緩存導(dǎo)致可見(jiàn)性問(wèn)題;
- 編譯優(yōu)化帶來(lái)的有序性問(wèn)題;
????那如何解決上面的2個(gè)問(wèn)題呢?那就是java內(nèi)存模型。
ps:還有一個(gè)問(wèn)題是:線程切換帶來(lái)的原子性問(wèn)題(后面會(huì)講到)
3、內(nèi)存模型概念
????Java 內(nèi)存模型規(guī)范了JVM 如何提供按需禁用緩存和編譯優(yōu)化的方法。具體來(lái)說(shuō),這些方法包括 volatile、synchronized 和 final 三個(gè)關(guān)鍵字,以及六項(xiàng) Happens-Before 規(guī)則。本文只講volatile。
????關(guān)于Java內(nèi)存模型網(wǎng)上講的概念太多了 ,感覺(jué)文縐縐的,反正我是看的比較煩,看完也記不住。
ps:網(wǎng)上很多內(nèi)容都來(lái)自于《深入理解Java虛擬機(jī)》,這本書(shū)也建議你看看。
????我這里就簡(jiǎn)單提取下幾個(gè)重要的概念,然后整整實(shí)例。
????Java內(nèi)存模型的主要目標(biāo)是定義程序中變量的訪問(wèn)規(guī)則:即在虛擬機(jī)中將變量存儲(chǔ)到主內(nèi)存或者將變量從主內(nèi)存取出這樣的底層細(xì)節(jié)。
- 主內(nèi)存:java虛擬機(jī)規(guī)定所有的變量(不是程序中的變量)都必須在主內(nèi)存中產(chǎn)生,為了方便理解,可以認(rèn)為是堆區(qū)。
- 工作內(nèi)存:java虛擬機(jī)中每個(gè)線程都有自己的工作內(nèi)存,該內(nèi)存是線程私有的為了方便理解,可以認(rèn)為是虛擬機(jī)棧。線程的工作內(nèi)存保存了線程需要的變量在主內(nèi)存中的副本。虛擬機(jī)規(guī)定:線程對(duì)主內(nèi)存變量的修改必須在線程的工作內(nèi)存中進(jìn)行,不能直接讀寫(xiě)主內(nèi)存中的變量。不同的線程之間也不能相互訪問(wèn)對(duì)方的工作內(nèi)存。如果線程之間需要傳遞變量的值,必須通過(guò)主內(nèi)存來(lái)作為中介進(jìn)行傳遞。
4、volatile解決緩存帶來(lái)的可見(jiàn)性問(wèn)題
先看一段程序:
public class TestAddCount {
private long count = 0;
public static void main(String[] args) throws InterruptedException {
TestAddCount testAddCount = new TestAddCount();
// 創(chuàng)建2個(gè)子線程,并執(zhí)行add操作
Thread t1 = new Thread(() -> {
testAddCount.add(10);
});
Thread t2 = new Thread(() -> {
testAddCount.add(10);
});
// 啟動(dòng)2個(gè)子線程
t1.start();
t2.start();
// main線程等待2個(gè)子線程執(zhí)行完
t1.join();
System.out.println("線程t1結(jié)束時(shí):" + testAddCount.count);
t2.join();
// 打印執(zhí)行后的結(jié)果count
System.out.println("線程t2結(jié)束時(shí):" + testAddCount.count);
}
private void add(int n) {
for (int i = 0; i < n; i++) {
try {
// 【注意:2個(gè)子線程并不是同時(shí)啟動(dòng)的,是有先后順序的,為了盡可能保證2個(gè)線程啟動(dòng)時(shí)count=0,所以在add方法中讓它睡眠0.1秒】
// 當(dāng)然,為了讓參數(shù)n小的情況下效果更加明顯,add方法中讓它睡眠了0.1秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.count++;
}
}
}
????我們想要的運(yùn)行后count的最終結(jié)果是不是20?但是實(shí)際結(jié)果總是小于等于20的。為什么呢?
????在單核時(shí)代,上面的程序運(yùn)行結(jié)果總是20。(不要告訴我你的電腦是單核的哦)
????在多核時(shí)代就不一樣了。count變量是屬于對(duì)象TestAddCount的實(shí)例變量,存儲(chǔ)于堆區(qū),是線程共享的。
????CPU為了平衡和內(nèi)存之間的速度差異,增加了緩存技術(shù)。
????如下圖:線程在讀取變量count的時(shí)候,會(huì)先將count的值讀入線程私有的緩存中,待處理結(jié)束后,再將緩存中的值寫(xiě)入內(nèi)存中。比如線程t1和t2剛開(kāi)始讀取到的count值都是0,然后分別進(jìn)行加1操作。假設(shè)t1先將結(jié)果1回寫(xiě)到內(nèi)存,然后t2再將結(jié)果1回寫(xiě)到內(nèi)存,進(jìn)行2次加1操作后,結(jié)果還是1。所以上面的例子得到的最終結(jié)果是小于等于20的。這就是緩存導(dǎo)致的可見(jiàn)性問(wèn)題。

如何解決呢?
????那就是按需禁用緩存,使用關(guān)鍵字volatile來(lái)修飾變量count即可。volatile修飾的變量,能保證新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。
private volatile long count = 0;
認(rèn)真的朋友可能發(fā)現(xiàn) ,加了volatile還是不行哦。因?yàn)檫€有一個(gè)問(wèn)題,就是原子性問(wèn)題。
這也便涉及到一道經(jīng)典的面試題:i++是原子性的嘛?
i++并不是一個(gè)原子性的操作。i++做了三次指令操作:
- 第一次,從內(nèi)存中讀取i變量的值到CPU的寄存器;
- 第二次,在寄存器中的i自增1
- 第三次,將寄存器中的值寫(xiě)入內(nèi)存。
????操作系統(tǒng)的任務(wù)切換是在CPU指令級(jí)別的,而不是高級(jí)語(yǔ)言里面的一條語(yǔ)句。如下圖:對(duì)于++i,在多核機(jī)器上,線程t1、t2在讀取內(nèi)存時(shí)也可能同時(shí)讀到同一個(gè)值,然后可能發(fā)生線程切換,這樣就會(huì)同一個(gè)值自增兩次,而實(shí)際上只自增了一次,所以++i也不是原子操作。這也就是線程切換帶來(lái)的原子性問(wèn)題。

????如何解決原子性的問(wèn)題,你可能想到了用synchronized來(lái)加鎖,保證其是原子性操作,synchronized的內(nèi)容比較多,回頭單獨(dú)整理一篇吧!?。?/p>
5、volatile解決編譯優(yōu)化帶來(lái)的有序性問(wèn)題
????上面說(shuō)了volatile還能解決編譯優(yōu)化帶來(lái)的有序性問(wèn)題,那么我們?cè)賮?lái)看下一個(gè)經(jīng)典的雙重校驗(yàn)來(lái)創(chuàng)建單例模式的例子:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
看下程序運(yùn)行流程:
- 假設(shè)兩個(gè)線程 t1、t2 同時(shí)調(diào)用 getInstance(),同時(shí)發(fā)現(xiàn) instance == null ;
- 于是同時(shí)對(duì) Singleton.class 加鎖,此時(shí) JVM 保證只有一個(gè)線程能夠加鎖成功(假設(shè)是線程 t1),另外一個(gè)線程則會(huì)處于等待狀態(tài)(假設(shè)是線程 t2);
- 線程 t1 會(huì)創(chuàng)建一個(gè) Singleton 實(shí)例,之后釋放鎖,鎖釋放后,線程 t2 被喚醒,線程t2 再次嘗試加鎖,此時(shí)是可以加鎖成功的;
- t2 加鎖成功后,線程 t2 再次檢查 instance == null 是否成立,發(fā)現(xiàn) instance!=null,不會(huì)再去實(shí)例化instance,直接返回線程t1實(shí)例化的instance。
????是不是覺(jué)得上面的程序沒(méi)有任何bug?但這個(gè) getInstance() 方法并不完美。問(wèn)題出在new操作上,我們以為的new操作是:
- 指令1:分配一塊內(nèi)存 X;
- 指令2:在內(nèi)存 X 上初始化 Singleton 對(duì)象;
- 指令3:然后 X 的地址賦值給 instance 變量。
????但是實(shí)際上優(yōu)化后的new卻是這樣的:
- 指令1:分配一塊內(nèi)存 X;
- 指令2:將 X 的地址賦值給 instance 變量;
- 指令3:最后在內(nèi)存 X 上初始化 Singleton 對(duì)象。
????優(yōu)化后會(huì)導(dǎo)致什么問(wèn)題呢?
????依然假設(shè)線程 t1 先去執(zhí)行 getInstance() 方法(和線程t2并不是同時(shí)去執(zhí)行),直接獲取到鎖,并執(zhí)行實(shí)例化new操作,當(dāng)執(zhí)行完指令 2 時(shí)恰好發(fā)生了線程切換,切換到了線程 t2 上;
如果此時(shí)線程 t2 也執(zhí)行 getInstance() 方法,那么線程 t2 在執(zhí)行第一個(gè)判斷時(shí)會(huì)發(fā)現(xiàn) instance != null ,所以直接返回 instance,而此時(shí)的 instance 是沒(méi)有初始化過(guò)的,如果我們這個(gè)時(shí)候訪問(wèn) instance 的成員變量就可能觸發(fā)空指針異常。這就是編譯優(yōu)化帶來(lái)的有序性問(wèn)題。
這個(gè)時(shí)候你就可以使用volatile來(lái)修飾變量instance,按需禁用編譯優(yōu)化。上面的有序性也就解決了哈。
static volatile Singleton instance;
總結(jié)

滬漂程序員一枚。
堅(jiān)持寫(xiě)博客,如果覺(jué)得還可以的話(huà),給個(gè)小星星哦,你的支持就是我創(chuàng)作的動(dòng)力。
個(gè)人微信公眾號(hào):“Java尖子生”,閱讀更多干貨。
<font color='red'>關(guān)注公眾號(hào),領(lǐng)取學(xué)習(xí)、面試資料。加技術(shù)討論群。</font>