JMM學習
學習資料:
一、系統CPU和主內存交互圖:
二、JMM模型
Java線程內存模型跟cpu緩存模型類似,是基於cpu緩存模型來建立的,java線程內存模型是標準化的,屏蔽掉了底層不同計算機的區別
注意:每個線程操作共享變量操作的是複製主內存的副本,當一個線程修改了自己工作內存中變量,對其他線程是不可見的,會導致線程不安全的問題。下面代碼演示這種問題:
代碼測試:
package com.lxf.volatileT;
import java.util.concurrent.TimeUnit;
/**
* 兩個線程訪問同一個共享變量,一個線程修改完共享變量後,另一個線程對這個共享變量是不可見的
*/
public class JMMDemo {
private static boolean flag=true;
public static void main(String[] args) {
new Thread(()->{//線程1
while (flag){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=false;
System.out.println("flag="+flag);
}
}
結果:打印flag=true,但是線程1並未停止
解決方法:
package com.lxf.volatileT;
import java.util.concurrent.TimeUnit;
/**
* volatile實現實現之間共享變量可見
*volatile作用:線程之間可見、有序性(防止指令重排)、不能保證原子性
*/
public class JMMDemo {
private static volatile boolean flag=true;
public static void main(String[] args) {
new Thread(()->{//線程1
while (flag){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=false;
System.out.println("flag="+flag);
}
}
在打印flag=true之後,線程1也停止了
三、JMM數據原子操作、volatile原理、volatile特性
3.1、JMM數據原子操作
- read(讀取):從主內存讀取數據
- load(載入):將主內存讀取到的數據寫入工作內存
- use(使用):從工作內存讀取到的數據來計算
- assign(賦值):將計算好的值重新賦值到工作內存中
- store(存儲):將工作內存數據寫入主內存
- write(寫入):將sotre過去的變量值賦值給主內存中的變量
- lock(鎖定):將主內存變量加鎖,標識爲線程獨佔狀態
- unlock(解鎖):將主內存變量解鎖,解鎖後其它線程可以鎖定該變量
3.2、JMM對這八種指令的使用,制定瞭如下規則:
-
不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須write
-
不允許線程丟棄他最近的assign操作,即工作變量的數據改變了之後,必須告知主存
-
不允許一個線程將沒有assign的數據從工作內存同步回主內存
-
一個新的變量必須在主內存中誕生,不允許工作內存直接使用一個未被初始化的變量。就是懟變量實施use、store操作之前,必須經過assign和load操作
-
一個變量同一時間只有一個線程能對其進行lock。多次lock後,必須執行相同次數的unlock才能解鎖。
-
如果對一個變量進行lock操作,會清空所有工作內存中此變量的值,在執行引擎使用這個變量前,必須重新load或assign操作初始化變量的值
-
如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量。
-
對一個變量進行unlock操作之前,必須把此變量同步回主內存
3.3、查看底層彙編指令
- 下載顯示彙編代碼的插件放入jdk中
- java程序彙編代碼查看:-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileVisbilityTest.prepareData
3.4、根據彙編代碼探究Volatile緩存可見性實現原理
- 底層實現主要是通過彙編lock前綴指令,它會鎖定這塊內存區域的緩存(緩存行鎖定)並回寫到主內存。
- IA-32架構軟件開發者手冊對lock指令的解釋:
- 會將當前處理器緩存行的數據立即寫回到系統內存。
- 這個寫回內存的操作會引起在其他CPU裏緩存了該內存地址的數據無效(MESI協議)
3.5、volatile的特性
- 首先我們要知道併發編程的三大特性:可見性、原子性、有序性
- Volatile保證可見性與有序性,但是不保證原子性,保證原子性需要藉助synchronized、lock這樣的鎖機制
Volatile不保證原子性代碼測試:
package com.lxf.jmm;
public class VolatileAtomicTest{
public static volatile int num=0;
public 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++) {
threads[i]=new Thread(()->{
for (int j = 0; j < 1000; j++) {
increase();
}
});
threads[i].start();
}
//等所有線程執行完了再執行main
for (Thread thread : threads) {
thread.join();
}
System.out.println("num="+num);
}
}
結果是<=10000,多次運行就能看到。
分析原因:
num++是一組操作(假設num值初始值爲0),裏面分爲三步原子操作:讀取到num值,num值加1,將num值寫回緩存,這三步操作都有可能阻塞。假如一個線程讀取num值後阻塞,然後另一個線程執行完了:將num賦值爲1,然後這個線程繼續執行將值仍然返回爲1,這就造成了上面的問題。
參考博文:volatile爲什麼不能保證原子性
Volatile保證有序性測試(禁止指令重排)
package com.lxf.jmm;
import java.util.HashMap;
import java.util.Map;
public class VolatileSerialTest {
static int x=0,y=0;
public static void main(String[] args) throws InterruptedException {
//存結果的Map集合
Map<String,Integer> resultMap=new HashMap<>();
for (int i = 0; i < 1000000; i++) {
x=0;y=0;
resultMap.clear();
//第一個線程
Thread one=new Thread(()->{
int a=y;
x=1;
resultMap.put("a",a);
});
//第二個線程
Thread two=new Thread(()->{
int b=x;
y=1;
resultMap.put("b",b);
});
//第一個線程開啓
one.start();
//第二個線程開啓
two.start();
//第一個線程插隊
one.join();
//第二個線程插隊
two.join();
if(resultMap.get("a")==0&&resultMap.get("b")==0){
System.out.println("===============a等於0,b也等於0================================");
System.out.println("a=" + resultMap.get("a") + ",b=" + resultMap.get("b"));
System.out.println("===============a等於0,b也等於0================================");
}else if(resultMap.get("a")==1&&resultMap.get("b")==1){
System.out.println("===============a等於1,b也等於1================================");
System.out.println("a=" + resultMap.get("a") + ",b=" + resultMap.get("b"));
System.out.println("===============a等於1,b也等於1================================");
}else{
System.out.println("a=" + resultMap.get("a") + ",b=" + resultMap.get("b"));
}
}
}
}
結果可能性:
a=0,b=0
a=0,b=1
a=1,b=0
a=1,b=1
結果分析:前三個可能性我們都知道,當one線程在前結果就是a=1,b=0,當two線程在前結果就是a=0,b=1,當兩個線程同時運行:a=0,b=0。但是a=1,b=1這種可能性還是難以理解,但還是存在的(很少見,得大量運行)見下圖:
因爲發生了指令重排序:
解決方法:定義x,y的時候加上volatile關鍵字就可以解決了
具體解釋看以下博文:
參考博文:
1. 併發關鍵字volatile(重排序和內存屏障)