文章目錄
- 一、JMM
- 1.1什麼是JMM
- 1.2記憶體劃分
- 1.3記憶體互動操作
- 二、Volatile
- 2.1 驗證 保證可見性
- 2.2 驗證 不保證原子性
- 2.3 驗證 禁止指令重排(有序性)
- 2.4小結
一、JMM
- JMM (java memory model) 即為JAVA 記憶體模型 ,不存在的東西,是一個概念,也是一個約定!
- 關于JMM的一些同步的約定:
- 1、線程解鎖前,必須把共享變量立刻刷回主存;
- 2、線程加鎖前,必須讀取主存中的最新值到工作記憶體中;
- 3、加鎖和解鎖是同一把鎖;
1.1什麼是JMM
- JMM即為JAVA 記憶體模型(java memory model)。因為在不同的硬體生産商和不同的作業系統下,記憶體的通路邏輯有一定的差異,結果就是當你的代碼在某個系統環境下運作良好,并且線程安全,但是換了個系統就出現各種問題。Java記憶體模型,就是為了屏蔽系統和硬體的差異,讓一套代碼在不同平台下能到達相同的通路結果。JMM從java 5開始的JSR-133釋出後,已經成熟和完善起來。
1.2記憶體劃分
- JMM規定了記憶體主要劃分為主記憶體和工作記憶體兩種。此處的主記憶體和工作記憶體跟JVM記憶體劃分(堆、棧、方法區)是在不同的層次上進行的,如果非要對應起來,主記憶體對應的是Java堆中的對象執行個體部分,工作記憶體對應的是棧中的部分區域,從更底層的來說,主記憶體對應的是硬體的實體記憶體,工作記憶體對應的是寄存器和高速緩存。

- JVM在設計時候考慮到,如果JAVA線程每次讀取和寫入變量都直接操作主記憶體,對性能影響比較大,是以每條線程擁有各自的工作記憶體,工作記憶體中的變量是主記憶體中的一份拷貝,線程對變量的讀取和寫入,直接在工作記憶體中操作,而不能直接去操作主記憶體中的變量。但是這樣就會出現一個問題,當一個線程修改了自己工作記憶體中變量,對其他線程是不可見的,會導緻線程不安全的問題。因為JMM制定了一套标準來保證開發者在編寫多線程程式的時候,能夠控制什麼時候記憶體會被同步給其他線程。
遇到問題:程式不知道主存中的值已經被修改過了!;
1.3記憶體互動操作
- 記憶體互動操作有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
- Volatile是 Java 虛拟機提供輕量級的同步機制,他的三大特性:
- 1、保證可見性 (這就要涉及JMM)
- 2、不保證原子性
- 3、禁止指令重排
下面我們來驗證三個特性
2.1 驗證 保證可見性
package com.wlw.Test_Volatile;
import java.util.concurrent.TimeUnit;
public class JMMDemo {
/**
* 這個程式一共有兩個線程: main線程 與 我們自己寫的一個A線程
* A線程中對變量num 進行判斷,如果為0,就一直循環,而main線程中将num 指派為1,此時我們沒有對變量num加volatile關鍵字,A線程一直循環下去
*
* 如果我們對變量num加volatile關鍵字,就保證了可見性:當main線程中對num進行修改,A線程可以看見修改後的值,進而進行相關操作
*/
private volatile static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num == 0){
}
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
2.2 驗證 不保證原子性
- 原子性:不可分割;
- 線程A在執行任務的時候,不能被打擾的,也不能被分割的,要麼同時成功,要麼同時失敗。
package com.wlw.Test_Volatile;
//volatile 不保證原子性
/**
* 一共20個線程,每個線程調用1000次add()方法,理論上num最後為20000
* 但是多線程操作,會出現同一時刻多個線程對num進行操作(因為在位元組碼檔案中num++ 這個操作被分為三步才執行完,不是原子操作),是以最後的值小于2萬
*
* ,加上volatile關鍵字之後,最後num結果依然小于20000,是以驗證了 volatile 不保證原子性。
* 但是如果我們對add()方法 加上synchronized 或者 lock鎖,是一定可以保證結果為2萬的,
* 但是問題是如果不加lock和synchronized (更耗費資源) ,怎麼樣保證原子性?
*/
public class VolatileDemo02 {
private volatile static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) {
// 一共20個線程,每個線程調用1000次add()方法,理論上num最後為20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j < 1000; j++) {
add();
}
add();
}).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "==>" + num);
}
}
- 問題:如果我們對add()方法 加上synchronized 或者 lock鎖,是一定可以保證結果為2萬的,但是問題是如果不加lock和synchronized (更耗費資源) ,怎麼樣保證原子性?
- 解決:使用 java.util.concurrent.atomic(原子包) 包下的原子類
- 這些類的底層都直接和作業系統挂鈎!是在記憶體中修改值。需要用到Unsafe類,而Unsafe類是一個很特殊的存在;(在21.CAS中介紹)
package com.wlw.Test_Volatile;
//volatile 不保證原子性
import java.util.concurrent.atomic.AtomicInteger;
/**
* 一共20個線程,每個線程調用1000次add()方法,理論上num最後為20000
* 但是多線程操作,會出現同一時刻多個線程對num進行操作(因為在位元組碼檔案中num++ 這個操作被分為三步才執行完,不是原子操作),是以最後的值小于2萬
* ,加上volatile關鍵字之後,最後num結果依然小于20000,是以 驗證了 volatile 不保證原子性。
* 但是如果我們對add()方法 加上synchronized 或者 lock鎖,是一定可以保證結果為2萬的,
* 但是問題是如果不加lock和synchronized (更耗費資源) ,怎麼樣保證原子性?
* 解決辦法:使用 java.util.concurrent.atomic(原子包) 包下的原子類
*/
public class VolatileDemo02 {
//private static int num = 0;
//使用原子類AtomicInteger 替換int
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
//num++;
num.getAndIncrement();//原子類的加1操作 , 裡面是調用的是native方法, 用的是底層的CAS(cpu的并發原語,效率極高)
}
public static void main(String[] args) {
// 一共20個線程,每個線程調用1000次add()方法,理論上num最後為20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j < 1000; j++) {
add();
}
add();
}).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "==>" + num);
}
}
2.3 驗證 禁止指令重排(有序性)
- 什麼是指令重排?
- 我們寫的程式,計算機并不是按照我們自己寫的那樣去執行的
- 源代碼–>編譯器優化重排–>指令并行也可能會重排–>記憶體系統也會重排–>執行
- 系統處理器在進行指令重排的時候,會考慮資料之間的依賴性!并不會随意地去排
int x=1; //1
int y=2; //2
x=x+5; //3
y=x*x; //4
//我們期望的執行順序是 1_2_3_4 指令重排後,可能執行的順序會變成2134 1324
//可不可能是 4123? 不可能的
-
看一個例子:
可能造成的影響結果:前提:a b x y這四個值 預設都是0
線程A | 線程B |
x=a | y=b |
b=1 | a=2 |
我們期望的 正常的結果: x = 0; y =0;
線程A | 線程B |
b=1 | a=2 |
x=a | y=b |
- volatile可以避免指令重排:是因為volatile中會加一道記憶體的屏障,這個記憶體屏障可以保證在這個屏障中的指令順序。
- 記憶體屏障,是CPU指令。作用:
- 1、保證特定的操作的執行順序;
- 2、可以保證某些變量的記憶體可見性(利用這些特性,就可以保證volatile實作的可見性)
2.4小結
- Volatile是可以保證可見性,不能保證原子性,由于記憶體屏障,可以保證避免指令重排的現象産生!
- Volatile 記憶體屏障在哪使用最多:在單例模式中使用最多(餓漢式,DCL懶漢式中用到了Volatile),連結為: