volatile和synchronized的差別全面了解Java記憶體模型
Votatile
文章目錄
- Votatile
- JMM
- Votatile
- 什麼是JMM
- Volatile
- 保證了可見性
- 不保證原子性
- 避免指令重排
- 指令重排
- volatile可以避免指令重排
- 單例模式
- 餓漢式
- DCL懶漢式
- DCL懶漢式
- 反射破環唯一性
- 解決: 繼續加鎖
- 但是雙反射繼續破環
- 繼續解決: 采用紅略燈經行.加标志進行判定
- 破環: 利用反射破環其私有權限
- 枚舉
- 通過讀取源碼得值,反射不能破環枚舉,利用枚舉
- 利用枚舉
- 注: 枚舉的最終反編譯源碼
JMM
Votatile
Votatile是java虛拟機輕量級的同步機制
- 保證了可見性
- 不保證原子性
- 禁止指令重排
什麼是JMM
JMM:java記憶體模型,不存在的東西,概念!!約定!!
關于JMM的一些同步的約定
- 線程解鎖前,必須把共享變量立即刷回主記憶體.
- 線性加鎖前,必須讀取主記憶體中的最新值到工作記憶體中
- 加鎖和解鎖是同一把鎖
線程 工作記憶體 、主記憶體
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAnYldHL0FWby9mZvwFN4ETMfdHLkVGepZ2XtxSZ6l2clJ3LcV2Zh1Wa9M3clN2byBXLzN3btgHL9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsQTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5COwEDN3YDNzgTY4EzN1IWNzYzX2EDNxITM4IzLcBTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
問題:程式不知道主記憶體的值已經被修改過了
記憶體互動操作有8種,虛拟機實作必須保證每一個操作都是原子的,不可在分的(對于double和long類型的變量來說,load、store、read和write操作在某些平台上允許例外)
- lock(鎖定):作用于主記憶體的變量,把一個變量辨別為線程獨占狀态
- unlock(解鎖):作用于主記憶體的變量,它把一個處于鎖定狀态的變量釋放出來,釋放後的變量才可以被其他線程鎖定
- read(讀取):作用于主記憶體變量,它把一個變量的值從主記憶體傳輸到線程的工作記憶體中,以便随後的load動作使用
- load(載入):作用于工作記憶體的變量,它把read操作從主存中變量放入工作記憶體中
- use(使用):作用于工作記憶體中的變量,它把工作記憶體中的變量傳輸給執行引擎,每當虛拟機遇到一個需要使用到變量的值,就會使用到這個指令
- assign(指派):作用于工作記憶體中的變量,它把一個從執行引擎中接受到的值放入工作記憶體的變量副本中
- store(存儲):作用于主記憶體中的變量,它把一個從工作記憶體中一個變量的值傳送到主記憶體中,以便後續的write使用
- write(寫入):作用于主記憶體中的變量,它把store操作從工作記憶體中得到的變量的值放入主内
存的變量中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操作之前,必須把此變量同步回主記憶體
問題:程式不知道主記憶體的值已經被修改過了
Volatile
- 保證了可見性
- 不保證原子性
- 禁止指令重排
保證了可見性
public class JMMDemo {
//不加volatile 程式就會死循環
//加volatile 保證了可見性
private volatile static int num=0;
public static void main(String[] args) {
new Thread(()->{
while (num==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(num);
}
}
不保證原子性
原子性: 不可分割
線程A在執行任務的時候,不能被打擾的,也不能被分割,要麼成功,要麼失敗
避免指令重排
指令重排
你寫的程式,計算機并不是按照你寫的那樣去執行的。
源代碼–>編譯器優化的重排–> 指令并行也可能會重排–>記憶體系統也會重排–> 執行
volatile可以避免指令重排
記憶體屏障.CPU指令,
- 保證特定的操作的執行順序
- 可以保證某些變量的記憶體可見性記憶體屏障:禁止上面指令和下面指令順序交換
Volatile是可以保持可見性。不能保證原子性,由于記憶體屏障,可以保證避免指令重排的現象産生!
單例模式
餓漢式DCL懶漢式
餓漢式
// 餓漢式單例
public class hun {
// 可能會浪費空間
private byte[] data1 = new byte[1024 * 1024];
private byte[] data2 = new byte[1024 * 1024];
private byte[] data3 = new byte[1024 * 1024];
private byte[] data4 = new byte[1024 * 1024];
private hun() {
}
private final static hun HUNGRY = new hun();
public static hun getInstance() {
return HUNGRY;
}
}
DCL懶漢式
DCL懶漢式
反射破環唯一性
單例不安全,反射可以進行破環
這裡的雙重檢測加鎖是保證了操作原子性,隻有一個線程能建立一個執行個體,其他線程無法建立第二個,volatie關鍵字是為了防止因為指令重排導緻的多線程問題,有可能A 建立了一個執行個體,虛拟機隻配置設定空間,對象引用這兩步,這既是線程B過來發現對象已經被建立了,但是擷取到的對象還是沒有被初始化
if (lazyMan == null) {
lazyMan = new LazyMan(); //不是原子性操作
/**
* 由于不是原子
*
* 1.配置設定記憶體空間
* 2.執行構造方法,初始化對象
* 3.把這個對象指向這個空間
*會出現指令重排
*
* 加上 volatile 不會出現指令重排
*/
}
解決: 繼續加鎖
但是雙反射繼續破環
繼續解決: 采用紅略燈經行.加标志進行判定
破環: 利用反射破環其私有權限
枚舉
通過讀取源碼得值,反射不能破環枚舉,利用枚舉
利用枚舉
枚舉有參構造
注: 枚舉的最終反編譯源碼