本文是兩章的筆記整理。html
CPU
緩存計算機中的全部運算操做都是由CPU
完成的,CPU
指令執行過程須要涉及數據讀取和寫入操做,可是CPU
只能訪問處於內存中的數據,而內存的速度和CPU
的速度是遠遠不對等的,所以就出現了緩存模型,也就是在CPU
和內存之間加入了緩存層。通常現代的CPU
緩存層分爲三級,分別叫L1
緩存、L2
緩存和L3
緩存,簡略圖以下:java
L1
緩存:三級緩存中訪問速度最快,可是容量最小,另外L1
緩存還被劃分紅了數據緩存(L1d
,data
首字母)和指令緩存(L1i
,instruction
首字母)L2
緩存:速度比L1
慢,可是容量比L1
大,在現代的多核CPU
中,L2
通常被單個核獨佔L3
緩存:三級緩存中速度最慢,可是容量最大,現代CPU
中也有L3
是多核共享的設計,好比zen3
架構的設計緩存的出現,是爲了解決CPU
直接訪問內存效率低下的問題,CPU
進行運算的時候,將須要的數據從主存複製一份到緩存中,由於緩存的訪問速度快於內存,在計算的時候只須要讀取緩存並將結果更新到緩存,運算結束再將結果刷新到主存,這樣就大大提升了計算效率,總體交互圖簡略以下:編程
雖然緩存的出現,大大提升了吞吐能力,可是,也引入了一個新的問題,就是緩存不一致。好比,最簡單的一個i++
操做,須要將內存數據複製一份到緩存中,CPU
讀取緩存值並進行更新,先寫入緩存,運算結束後再將緩存中新的刷新到內存,具體過程以下:緩存
i
到緩存中CPU
讀取緩存i
中的值i
進行加1操做這樣的i++
操做在單線程不會出現問題,但在多線程中,由於每一個線程都有本身的工做內存(也叫本地內存,是線程本身的緩存),變量i
在多個線程的本地內存中都存在一個副本,若是有兩個線程執行i++
操做:多線程
i
初始值爲0i
的值放入緩存中,此時i
的值爲0,線程B也同理,放入緩存中的值也是0i
的值都是1i
寫入主內存,至關於i
被兩次賦值爲1i
的值爲1這個就是典型的緩存不一致問題,主流的解決辦法有:架構
這是一種悲觀的實現方式,具體來講,就是經過處理器發出lock
指令,鎖住總線,總線收到指令後,會阻塞其餘處理器的請求,直到佔用鎖的處理器完成操做。特色是隻有一個搶到總線鎖的處理器運行,可是這種方式效率低下,一旦某個處理器獲取到鎖其餘處理器只能阻塞等待,會影響多核處理器的性能。併發
圖示以下:app
緩存一致性協議中最出名的就是MESI
協議,MESI
保證了每個緩存中使用的共享變量的副本都是一致的。大體思想是,CPU
操做緩存中的數據時,若是發現該變量是一個共享變量,操做以下:ide
CPU
將該變量的緩存行設置爲無效狀態(Invalid
),其餘CPU
進行該變量的讀取時須要到主存中再次獲取具體來講,MESI
中規定了緩存行使用4種狀態標記:高併發
M
:Modified
,被修改E
:Exclusive
,獨享的S
:Shared
,共享的I
:Invalid
,無效的有關MESI
詳細的實現超出了本文的範圍,想要詳細瞭解能夠參考此處或此處。
JMM
看完了CPU
緩存再來看一下JMM
,也就是Java
內存模型,指定了JVM
如何與計算機的主存進行工做,同時也決定了一個線程對共享變量的寫入什麼時候對其餘線程可見,JMM
定義了線程和主內存之間的抽象關係,具體以下:
JMM
內存模型同樣也是一個抽象概念,其實並不存在,涵蓋了緩存、寄存器、編譯期優化以及硬件等簡略圖以下:
與MESI
相似,若是一個線程修改了共享變量,刷新到主內存後,其餘線程讀取工做內存的時候發現緩存失效,會從主內存再次讀取到工做內存中。
而下圖表示了JVM
與計算機硬件分配的關係:
文章都看了大半了還沒到volatile
?別急別急,先來看看併發編程中的三個重要特性,這對正確理解volatile
有很大的幫助。
原子性就是在一次或屢次操做中:
一個典型的例子就是兩我的轉帳,好比A向B轉帳1000元,那麼這包含兩個基本的操做:
這兩個操做,要麼都成功,要麼都失敗,也就是不能出現A帳戶扣除1000可是B帳戶金額不變的狀況,也不能出現A帳戶金額不變B帳戶增長1000的狀況。
須要注意的是兩個原子性操做結合在一塊兒未必是原子性的,好比i++
。本質上來講,i++
涉及到了三個操做:
get i
i+1
set i
這三個操做都是原子性的,可是組合在一塊兒(i++
)就不是原子性的。
另外一個重要的特性是可見性,可見性是指,一個線程對共享變量進行了修改,那麼另外的線程能夠當即看到修改後的最新值。
一個簡單的例子以下:
public class Main { private int x = 0; private static final int MAX = 100000; public static void main(String[] args) throws InterruptedException { Main m = new Main(); Thread thread0 = new Thread(()->{ while(m.x < MAX) { ++m.x; } }); Thread thread1 = new Thread(()->{ while(m.x < MAX){ } System.out.println("finish"); }); thread1.start(); TimeUnit.MILLISECONDS.sleep(1); thread0.start(); } }
線程thread1
會一直運行,由於thread1
把x
讀入工做內存後,會一直判斷工做內存中的值,因爲thread0
改變的是thread0
工做內存的值,並無對thread1
可見,所以永遠也不會輸出finish
,使用jstack
也能夠看到結果:
有序性是指代碼在執行過程當中的前後順序,因爲JVM
的優化,致使了代碼的編寫順序未必是代碼的運行順序,好比下面的四條語句:
int x = 10; int y = 0; x++; y = 20;
有可能y=20
在x++
前執行,這就是指令重排序。通常來講,處理器爲了提升程序的效率,可能會對輸入的代碼指令作必定的優化,不會嚴格按照編寫順序去執行代碼,但能夠保證最終運算結果是編碼時的指望結果,固然,重排序也有必定的規則,須要嚴格遵照指令之間的數據依賴關係,並非能夠任意重排序,好比:
int x = 10; int y = 0; x++; y = x+1;
y=x+1
就不能先優於x++
執行。
在單線程下重排序不會致使預期值的改變,但在多線程下,若是有序性得不到保證,那麼將可能出現很大的問題:
private boolean initialized = false; private Context context; public Context load(){ if(!initialized){ context = loadContext(); initialized = true; } return context; }
若是發生了重排序,initialized=true
排序到了context=loadContext()
的前面,假設兩個線程A、B同時訪問,且loadContext()
須要必定耗時,那麼:
true
,再進行loadContext()
操做true
,會直接返回一個未加載完成的context
volatile
好了終於到了volatile
了,前面說了這麼多,目的就是爲了能完全理解和明白volatile
。這部分分爲四個小節:
volatile
的語義synchronized
區別先來介紹一下volatile
的語義。
被volatile
修飾的實例變量或者類變量具備兩層語義:
先說結論:
volatile
能保證可見性volatile
能保證有序性volatile
不能保證原子性下面分別進行介紹。
Java
中保證可見性有以下方式:
volatile
:當一個變量被volatile
修飾時,對共享資源的讀操做會直接在主內存中進行(準確來講也會讀取到工做內存中,可是若是其餘線程進行了修改就必須從主內存從新讀取),寫操做是先修改工做內存,可是修改結束後當即刷新到主內存中synchronized
:synchronized
同樣能保證可見性,可以保證同一時刻只有一個線程獲取到鎖,而後執行同步方法,而且確保鎖釋放以前,變量的修改被刷新到主內存中Lock
:Lock
的lock
方法能保證同一時刻只有一個線程可以獲取到鎖而後執行同步方法,而且確保鎖釋放以前可以將對變量的修改刷新到主內存中具體來講,能夠看一下以前的例子:
public class Main { private int x = 0; private static final int MAX = 100000; public static void main(String[] args) throws InterruptedException { Main m = new Main(); Thread thread0 = new Thread(()->{ while(m.x < MAX) { ++m.x; } }); Thread thread1 = new Thread(()->{ while(m.x < MAX){ } System.out.println("finish"); }); thread1.start(); TimeUnit.MILLISECONDS.sleep(1); thread0.start(); } }
上面說過這段代碼會不斷運行,一直沒有輸出,就是由於修改後的x
對線程thread1
不可見,若是在x
的定義中加上了volatile
,就不會出現沒有輸出的狀況了,由於此時對x
的修改是線程thread1
可見的。
JMM
中容許編譯期和處理器對指令進行重排序,在多線程的狀況下有可能會出現問題,爲此,Java
一樣提供了三種機制去保證有序性:
volatile
synchronized
Lock
另外,關於有序性不得不提的就是Happens-before
原則。Happends-before
原則說的就是若是兩個操做的執行次序沒法從該原則推導出來,那麼就沒法保證有序性,JVM
或處理器能夠任意重排序。這麼作的目的是爲了儘量提升程序的並行度,具體規則以下:
unlock
操做要先行發生於對同一個鎖的lock
操做volatile
變量規則:對一個變量的寫操做要早於對這個變量以後的讀操做Thread
對象的start()
方法先行發生於對該線程的任何動做interrupt()
方法確定要優於捕獲到中斷信號,換句話說,若是收到了中斷信號,那麼在此以前一定調用了interrupt()
finalize()
以前對於volatile
,會直接禁止對指令重排,可是對於volatile
先後無依賴關係的指令能夠隨意重排,好比:
int x = 0; int y = 1; //private volatile int z; z = 20; x++; y--;
在z=20
以前,先定義x
或先定義y
並無要求,只須要在執行z=20
的時候,能夠保證x=0,y=1
便可,同理,x++
或y--
具體先執行哪個並無要求,只須要保證二者執行在z=20
以後便可。
在Java
中,全部對基本數據類型變量的讀取賦值操做都是原子性的,對引用類型的變量讀取和賦值也是原子性的,可是:
i++
JMM
只保證基本讀取和賦值的原子性操做,其餘的均不保證,若是須要具有原子性,那麼可使用synchronized
或Lock
,或者JUC
包下的原子操做類也就是說,volatile
並不能保證原子性,例子以下:
public class Main { private volatile int x = 0; private static final CountDownLatch latch = new CountDownLatch(10); public void inc() { ++x; } public static void main(String[] args) throws InterruptedException { Main m = new Main(); IntStream.range(0, 10).forEach(i -> { new Thread(() -> { for (int j = 0; j < 1000; j++) { m.inc(); } latch.countDown(); }).start(); }); latch.await(); System.out.println(m.x); } }
最後輸出的x
的值會少於10000
,並且每次運行的結果也並不相同,至於緣由,能夠從兩個線程A、B開始分析,圖示以下:
0-t1
:線程A將x
讀入工做內存,此時x=0
t1-t2
:線程A時間片完,CPU
調度線程B,線程B將x
讀入工做內存,此時x=0
t2-t3
:線程B對工做內存中的x
進行自增操做,並更新到工做內存中t3-t4
:線程B時間片完,CPU
調度線程A,同理線程A對工做內存中的x
自增t4-t5
:線程A將工做內存中的值寫回主內存,此時主內存中的值爲x=1
t5
之後:線程A時間片完,CPU
調度線程B,線程B也將本身的工做內存寫回主內存,再次將主內存中的x
賦值爲1也就是說,多線程操做的話,會出現兩次自增可是實際上只進行一次數值修改的操做。想要x
的值變爲10000
也很簡單,加上synchronized
便可:
new Thread(() -> { synchronized (m) { for (int j = 0; j < 1000; j++) { m.inc(); } } latch.countDown(); }).start();
前面已經知道,volatile
能夠保證有序性以及可見性,那麼,具體是如何操做的呢?
答案就是一個lock;
前綴,該前綴實際上至關於一個內存屏障,該內存屏障會爲指令的執行提供以下幾個保障:
一個典型的使用場景是利用開關進行線程的關閉操做,例子以下:
public class ThreadTest extends Thread{ private volatile boolean started = true; @Override public void run() { while (started){ } } public void shutdown(){ this.started = false; } }
若是布爾變量沒有被volatile
修飾,那麼極可能新的布爾值刷新不到主內存中,致使線程不會結束。
synchronized
的區別volatile
只能用於修飾實例變量或者類變量,可是不能用於修飾方法、方法參數、局部變量等,另外能夠修飾的變量爲null
。但synchronized
不能用於對變量的修飾,只能修飾方法或語句塊,並且monitor
對象不能爲null
volatile
沒法保證原子性,可是synchronized
能夠保證volatile
與synchronized
都能保證可見性,可是synchronized
是藉助於JVM
指令monitor enter
/monitor exit
保證的,在monitor exit
的時候全部共享資源都被刷新到主內存中,而volatile
是經過lock;
機器指令實現的,迫使其餘線程工做內存失效,須要到主內存加載volatile
可以禁止JVM
以及處理器對其進行重排序,而synchronized
保證的有序性是經過程序串行化執行換來的,而且在synchronized
代碼塊中的代碼也會發生指令重排的狀況volatile
不會使線程陷入阻塞,但synchronized
會