文章目錄
- 可見性
- 原子性
- 有序性(指令重排)
- 經典的指令重排案例:單例模式的雙重檢查鎖
- volatile和synchronize都可以保證有序性
- 并發壓測工具Jcstress證明指令重排會在多線程下出現問題(了解)
- CPU緩存分為三個級別:L1、L2、L3
- 寄存器
- 緩存和寄存器的區別
- JMM(java memory modle)
可見性
原子性
并發編程時,當一個線程對共享變量的修改操作進行到一半時,另一個線程也可能來操作共享變量,這時就會干擾前一個線程的操作,這也就是原子性問題。
public class AtomicDemo {private static int num = 0;public static void main(String[] args) {List<Thread> list = new ArrayList<>();for (int i = 0; i < 5; i++) {Thread thread = new Thread(() -> {for (int j = 0; j < 1000; j++) {num++;}});list.add(thread);thread.start();}list.forEach(e -> {try {e.join();} catch (InterruptedException ex) {ex.printStackTrace();}});System.out.println(num);}
}
上面代碼的運行結果不一定是5000,原因分析如下:
i++編譯后對應的JVM指令實際有4條,當只執行了部分操作時,另一個線程同時操作i變量,就會出現原子性問題
有序性(指令重排)
指令重排:為了保證程序的執行效率,在不影響正確性的前提下,編譯器和CPU會對程序中代碼進行優化,即指令重排序
- 指令重排必須保證單線程情況下程序的運行結果是正確的
- 指令重排在多線程下可能會影響程序運行結果的正確性。
經典的指令重排案例:單例模式的雙重檢查鎖
volatile和synchronize都可以保證有序性
- synchronize:保證了只有一個線程在操作同步代碼塊內的代碼,而指令重排在單線程的情況下運行結果是正確的
并發壓測工具Jcstress證明指令重排會在多線程下出現問題(了解)
<dependency><groupId>org.openjdk.jcstress</groupId><artifactId>jcstress-core</artifactId><version>0.7</version><scope>test</scope>
</dependency>
/**
* 需求:測試指令重排導致程序結果異常情況
* 方法test2中,有可能 flag=true先執行,而num=2后執行,位置交換導致出現方法test1結果r.r1=0
*/
@JCStressTest
//表示對輸出結果的處理 Expect.ACCEPTABLE 可以接收的結果
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
//Expect.ACCEPTABLE_INTERESTING 表示可以接收,并感興趣的結果
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class jcstress {int num = 0;boolean flag = false;//線程1執行的代碼@Actorpublic void test1(I_Result r) {if (flag) {r.r1 = num + num;} else {r.r1 = 1;}}//線程2執行的代碼@Actorpublic void test2(I_Result r) {num = 2;flag = true;}
}
CPU緩存分為三個級別:L1、L2、L3
CPU的運算速度和內存的訪問速度相差比較大,導致CPU每次操作內存都需要耗費大量的等待時間,于是CPU和內存直接增加了緩存設計。
(1)L1(一級緩存)是最接近CPU的,三個緩存中它容量最小,速度最快,每個物理內核上都有個一級緩存L1
(2)L2(二級緩存)速度比L1慢,比L3快,一般情況下每個物理核上都有一個獨立的L2
(3)L3(三級緩存)是三個緩存中最大的,同時也是速度最慢的,同一個CPU插槽上的核共用一個三級緩存
寄存器
CPU和一級緩存之間還有寄存器,CPU經常使用同一內存地址的某數據時 ,為減少頻繁讀取的消耗,就會把該數據存儲到寄存器。
緩存和寄存器的區別
(1)緩存是把CPU需要的數據提前緩存起來,減少讀取的消耗,但不一定是經常使用的
(2)寄存器是把CPU經常使用的同一內存地址的數據緩存起來,減少讀取消耗
JMM(java memory modle)
java內存模型和java內存結構不是一回事,java內存模型用于多線程讀寫共享數據時,保證共享數據的可見性、有序性、原子性,主要是通過synchronize、volatile兩個關鍵字來實現
(1)主內存:主內存是所有線程都能訪問,所有共享變量都存儲在主內存—方法區和堆
(2)工作內存:每個線程都有自己的工作內存,只存儲該線程需要用到的共享變量的副本,線程對變量的所有操作都是工作內存完成的,而不是直接讀寫主內存的變量,不同線程之間也不能相互訪問工作內存中的變量。
(3)jMM內存模型和硬件內存不是一回事,它是一個抽象的概念,不管是工作內存的數據還是主內存的數據,即可能存儲到內存,也可能存到CPU的三級緩存或者寄存器中。