并發(fā)編程的原理
課程目標
- JMM 內(nèi)存模型
- JMM 如何解決原子性、可見性、有序性的問題
-
Synchronized和volatile
回顧
線程的轉(zhuǎn)換,線程的停止?;?CPU 的內(nèi)存模型,硬件架構(gòu),高速緩存,和它的一些線程的并行執(zhí)行所帶來的問題,在 CPU 層面上提供了解決方案,比如說 總線鎖、緩存鎖的方式解決這些問題。
在 JAVA 層面,統(tǒng)一了規(guī)范,JMM 定義了共享內(nèi)存系統(tǒng)中多個線程同時訪問內(nèi)存時的規(guī)范。去屏蔽硬件和操作系統(tǒng)的內(nèi)存訪問的差異。它和 JVM 是有點類似的。 JVM 的出現(xiàn)是為了提供了一個在操作系統(tǒng)層面上一個虛擬機,他可以真正地實現(xiàn)一次編譯,到處運行的效果。JMM 也是類似的道理,他最終實現(xiàn)了 JAVA 程序在各個平臺下都能夠?qū)崿F(xiàn)一致的內(nèi)存訪問效果。
[圖片上傳失敗...(image-a3ffea-1741101421427)]
在 JMM 定義了 8 種內(nèi)存的操作。
lock 就是鎖定,鎖定我們的主內(nèi)存的變量,保證他變成一個線程的獨占狀態(tài),這個是一個開放式的指令。

CPU 層面的解決方式是總線鎖和緩存鎖。
而 JMM 是在我們的 CPU 和我們的應(yīng)用層之間抽象的一個模型,去解決底層的一個問題。
- 多線程通訊的兩種方式
共享內(nèi)存
-
消息傳遞
內(nèi)存中存在一個共享變量的值,當(dāng)多個線程訪問主內(nèi)存的時候,一個線程 1 先去改變工作內(nèi)存從 1 -> 2,主內(nèi)存從 1 變成 2, 線程2 去訪問的時候,就變成 2 。這是消息共享內(nèi)存的傳遞方式。
消息傳遞,就是
wait/notify,這種就是線程中間沒有公共狀態(tài),線程之間去發(fā)送消息,它都是通過一種阻塞、等待、釋放鎖的方式,去喚醒去改變共享變量的通訊的數(shù)據(jù)。
在 JMM 模型中會有一個問題,什么時候同步到主內(nèi)存,什么時候同步到另一個線程的內(nèi)存。
可見性問題?
-
原子性問題
當(dāng)我們線程同時去訪問共享變量的時候,當(dāng)兩個線程同時運行,同時去對這個值去
+ 1,最后結(jié)果 不對,導(dǎo)致的原子性問題。 -
有序性?
包含編譯器和 CPU 層面的有序性的問題。
JMM 是基于我們物理模型的抽象。抽象內(nèi)存模型在硬件抽象模型里有它的映射關(guān)系。
-
主內(nèi)存JVM 層面的堆內(nèi)存,堆內(nèi)存是從我們的物理內(nèi)存里邊去劃分一塊內(nèi)存去分配給這個進程。物理內(nèi)存的一部分。 -
工作內(nèi)存CPU 的高速緩存和 CPU 的寄存器。
CPU 層面上有解決方案,但是 JMM 怎么去解決。
有序性問題原因
- 編譯器的指令重排序
- 處理器的指令重排序
- 內(nèi)存系統(tǒng)的重排序,(內(nèi)存訪問的順序性,多線程訪問內(nèi)存是沒有順序的。)
JMM怎么解決原子性、可見性、有序性的問題?
在 JAVA 中提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字,比如 `volatile` 、 `Synchronized` 、 `final` 、 `j.u.c` 等,這些就是Java內(nèi)存模型封裝了底層的實現(xiàn)后提供給開發(fā)人員使用的關(guān)鍵字,在開發(fā)多線程代碼的時候,我們可以 `synchronized` 等關(guān)鍵詞來控制并發(fā),使得我們不需要關(guān)心底層的編譯器優(yōu)化、緩存一致性的問題了,所以在JAVA 內(nèi)存模型中,除了定義了一套規(guī)范,還提供了開放的指令在底層進行封裝后,提供給開發(fā)人員使用。
-
Synchronized是“萬能”的。
原子性保障
在 **JAVA** 中提供了兩個高級的字節(jié)碼指令 `monitorenter` 和 `monitorexit` ,在Java中對應(yīng)的 `Synchronized` 來保證代碼塊內(nèi)的操作是原子的。
可見性
**JAVA** 中的 `volatile` 關(guān)鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次使用之前都從主內(nèi)存刷新。因此,可以使用volatile來保證多線程操作時變量的可見性。除了`volatile`,JAVA中的 `synchronized` 和 `final` 兩個關(guān)鍵字也可以實現(xiàn)可見性。
有序性
在 JAVA 中,可以使用 `synchronized` 和 `volatile` 來保證多線程之間操作的有序性。實現(xiàn)方式有所區(qū)別:
volatile 關(guān)鍵字會禁止指令重排。 synchronized 關(guān)鍵字保證同一時刻只允許一條線程操作。
volatile如何保證可見性
volatile 是一個輕量級的鎖。(解決可見性、防止指令重排)
下載hsdis工具 ,https://sourceforge.net/projects/fcml/files/fcml-1.1.1/hsdis-1.1.1-win32-amd64.zip/download
解壓后存放到j(luò)re目錄的server路徑下
JavaGuide_并發(fā)編程_原理1_hisdis放在jre中.pngJDK 下邊的 JRE 就行
JavaGuide_并發(fā)編程_原理1_ide配置.png然后跑main函數(shù),跑main函數(shù)之前,加入如下虛擬機參數(shù):
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*App.getInstance(替換成實際運行的代碼)
public class ThreadDemo {
private static volatile ThreadDemo instance = null;
public static ThreadDemo getInstance() {
if (instance==null){
instance = new ThreadDemo();
}
return instance;
}
public static void main(String[] args) {
ThreadDemo.getInstance();
}
}
0x0000000002bf6098: lock add dword ptr [rsp],0h ;*putstatic instance
; - com.darian.multiplethread2.ThreadDemo::getInstance@13 (line 8)
沒有加 volatile ,是沒有鎖的。
`volatile` 變量修飾的共享變量,在進行寫操作的時候會多出一個 `lock` 前綴的匯編指令,這個指令在前面我們講解CPU高速緩存的時候提到過,會觸發(fā)總線鎖或者緩存鎖,通過緩存一致性協(xié)議來解決可見性問題對于聲明 `volatile` 的變量進行寫操作,JVM就會向處理器發(fā)送一條Lock前綴的指令,把這個變量所在的緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存,再根據(jù)我們前面提到過的 **MESI** 的緩存一致性協(xié)議,來保證多 CPU 下的各個高速緩存中的數(shù)據(jù)的一致性。
volatile防止指令重排序
指令重排的目的是最大化的提高CPU利用率以及性能,CPU的亂序執(zhí)行優(yōu)化在單核時代并不影響正確性,但是在多核時代的多線程能夠在不同的核心上實現(xiàn)真正的并行,一旦線程之間共享數(shù)據(jù),就可能會出現(xiàn)一些不可預(yù)料的問題指令重排序必須要遵循的原則是,不影響代碼執(zhí)行的最終結(jié)果,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序,(這里所說的數(shù)據(jù)依賴性僅僅是針對單個處理器中執(zhí)行的指令和單個線程中執(zhí)行的操作.)這個語義,實際上就是 `as-if-serial` 語義,不管怎么重排序,單線程程序的執(zhí)行結(jié)果不會改變,編譯器、處理器都必須遵守 `as-if-serial` 語義。
-
public class VolatileDemo { public static void main(String[] args) { // as-if-serial int a = 2; int b = 3; int c = a + b; } }編譯器在編譯的時候,以及 CPU 在執(zhí)行的時候,都會存在相應(yīng)的指令執(zhí)行,所以在編譯以后,可能會對我們的指令做一個順序的調(diào)整??赡軙葓?zhí)行
b = 3,在去執(zhí)行a = 2,最終會滿足不會影響最終的運行結(jié)果。最終的結(jié)果是不會變的。
多核心多線程下的指令重排影響
public class VolatileSortDemo {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("[x=" + x + "]\t[y=" + y + "]");
}
}
如果不考慮編譯器重排序和緩存可見性問題,上面這段代碼可能會出現(xiàn)的結(jié)果是
x=0,y=1;
x=1,y=0;
-
x=1,y=1
這三種結(jié)果,既可能是先后執(zhí)行t1/t2,也可能是反過來,還可能是t1/t2交替執(zhí)行,但是這段代碼的執(zhí)行結(jié)果也有可能是
x=0,y=0。這就是在亂序執(zhí)行的情況下會導(dǎo)致的一種結(jié)果。因為線程t1內(nèi)部的兩行代碼之間不存在數(shù)據(jù)依賴,因此可以把x=b亂序到a=1之前。同時線程 t2 中的y=a也可以早于t1中的a=1執(zhí)行,那么他們的執(zhí)行順序可能是。
[圖片上傳失敗...(image-19ef3e-1741101421427)]
t1: x=b
t2:b=1
t2:y=a
-
t1:a=1
所以從上面的例子來看,重排序會導(dǎo)致可見性問題。但是重排序帶來的問題的嚴重性遠遠大于可見性,因為并不是所有指令都是簡單的讀或?qū)?,比?**DCL** 的部分初始化問題。所以單純地解決可見性問題還不夠,還需要解決處理器重排序問題。
DCL 的問題
- 可能會存在指令重排序的半內(nèi)存、不完整對象的問題。
提供了一種解決方式叫內(nèi)存屏障。
#join 底層是基于 wait notify 來實現(xiàn)的。
內(nèi)存屏障
內(nèi)存屏障需要解決我們前面提到的兩個問題。一個是編譯器的優(yōu)化亂序和CPU的執(zhí)行亂序,我們可以分別使用 `優(yōu)化屏障` 和 `內(nèi)存屏障` 這兩個機制來解決。
- 優(yōu)化屏障
- 內(nèi)存屏障
從CPU層面來了解一下什么是內(nèi)存屏障
CPU的亂序執(zhí)行,本質(zhì)還是,由于在多CPU的機器上,每個CPU都存在cache,當(dāng)一個特定數(shù)據(jù)第一次被特定一個CPU獲取時,由于在該CPU緩存中不存在,就會從內(nèi)存中去獲取,被加載到CPU高速緩存中后就能從緩存中快速訪問。當(dāng)某個CPU進行寫操作時,它必須確保其他的CPU已經(jīng)將這個數(shù)據(jù)從他們的緩存中移除,這樣才能讓其他CPU 安全地修改數(shù)據(jù)。顯然,存在多個cache時,我們必須通過一個cache一致性協(xié)議來避免數(shù)據(jù)不一致的問題,而這個通訊的過程就可能導(dǎo)致亂序訪問的問題,也就是運行時的內(nèi)存亂序訪問?,F(xiàn)在的CPU架構(gòu)都提供了內(nèi)存屏障功能,在 **x86的cpu** 中,實現(xiàn)了相應(yīng)的內(nèi)存屏障寫屏障(store barrier)、讀屏障(load barrier)和 全屏障(Full Barrier),主要的作用是。
- 防止指令之間的重排序
- 保證數(shù)據(jù)的可見性
編譯的時候會進行優(yōu)化,執(zhí)行的時候亂序執(zhí)行。
instance = new ThreadDemo();分為 3 個操作,分配內(nèi)存,指向地址,初始化。
store barrier

`store barrier`稱為 寫屏障 ,相當(dāng)于 `storestore barrier` , 強制所有在 storestore 內(nèi)存屏障之前的所有指令先執(zhí)行。都要在該內(nèi)存屏障之前執(zhí)行,并發(fā)送緩存失效的信號。所有在 `storestore barrier` 指令之后的store 指令,都必須在 `storestore barrier` 屏障之前的指令執(zhí)行完后再被執(zhí)行。也就是禁止了寫屏障前后的指令進行重排序,使得所有 `store barrier` 之前發(fā)生的內(nèi)存更新都是可見的(這里的可見指的是修改值可見以及操作結(jié)果可見)
load barrier
[圖片上傳失敗...(image-2fa6cd-1741101421427)]
`load barrier`稱為讀屏障,相當(dāng)于`loadload barrier` ,強制所有在 `load barrier` 讀屏障之后的 `load` 指令,都在 `load barrier` 屏障之后執(zhí)行。也就是禁止對 `load barrier` 讀屏障前后的 `load` 指令進行重排序, 配合 `store barrier` ,使得所有 `store barrier` 之前發(fā)生的內(nèi)存更新,對 `load barrier` 之后的 `load` 操作是可見的。
Full barrier

`full barrier` 稱為全屏障,相當(dāng)于 `storeload` ,是一個全能型的屏障,因為它同時具備前面兩種屏障的效果。強制了所有在 `storeload barrier` 之前的 `store/load` 指令,都在該屏障之前被執(zhí)行,所有在該屏障之后的的 `store/load` 指令,都在該屏障之后被執(zhí)行。禁止對 `storeload` 屏障前后的指令進行重排序。
總結(jié)
內(nèi)存屏障只是解決 **順序一致性問題** ,不解決 **緩存一致性問題** ,緩存一致性 是由 **cpu的緩存鎖** 以及 **MESI** 協(xié)議來完成的。而緩存一致性協(xié)議只關(guān)心緩存一致性,不關(guān)心順序一致性。所以這是兩個問題。編譯器層面如何解決指令重排序問題。
編譯層如何解決指令重排序問題?
在編譯器層面,通過volatile關(guān)鍵字,取消編譯器層面的緩存和重排序。保證編譯程序時在優(yōu)化屏障之前的指令不會在優(yōu)化屏障之后執(zhí)行。這就保證了編譯時期的優(yōu)化不會影響到實際代碼邏輯順序。如果硬件架構(gòu)本身已經(jīng)保證了內(nèi)存可見性,那么 `volatile` 就是一個空標記,不會插入相關(guān)語義的內(nèi)存屏障。如果硬件架構(gòu)本身不進行處理器重排序,有更強的重排序語義,那么 `volatile` 就是一個空標記,不會插入相關(guān)語義的內(nèi)存屏障。
在 **JMM** 中把內(nèi)存屏障指令分為4類,通過在不同的語義下使用不同的內(nèi)存屏障來禁止特定類型的處理器重排序,從而來保證內(nèi)存的可見性
- loadload barrier
- storestore barrier
- loadstore barrier
- storeload barrier
LoadLoad Barriers, load1 ; LoadLoad; load2 , 確保load1數(shù)據(jù)的裝載優(yōu)先于load2及所有后續(xù)裝載指令的裝載
StoreStore Barriers, store1; storestore;store2 , 確保store1數(shù)據(jù)對其他處理器可見優(yōu)先于store2及所有后續(xù)存儲
指令的存儲
LoadStore Barries, load1;loadstore;store2, 確保load1數(shù)據(jù)裝載優(yōu)先于store2以及后續(xù)的存儲指令刷新到內(nèi)存
StoreLoad Barries, store1; storeload;load2, 確保store1數(shù)據(jù)對其他處理器變得可見, 優(yōu)先于load2及所有后續(xù)
裝載指令的裝載;這條內(nèi)存屏障指令是一個全能型的屏障,在前面講cpu層面的內(nèi)存屏障的時候有提到。它同時具有其他3條屏障的效果。
volatile為什么不能保證原子性
public class Demo {
static volatile int i;
public static void main(String[] args) {
i = 10;
}
}
然后通過 javap -c Demo.class ,去查看字節(jié)碼
{
static volatile int i;
descriptor: I
flags: (0x0048) ACC_STATIC, ACC_VOLATILE
ACC_VOLATILE
accessFlags.hpp
bool is_volatile () const { return (_flags & JVM_ACC_VOLATILE ) != 0; }
bytecodeinterpreter.cpp
// 把結(jié)果寫回到棧中
// Now store the result on the stack
//
TosState tos_type = cache->flag_state();
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
if (tos_type == atos) {
VERIFY_OOP(obj->obj_field_acquire(field_offset));
SET_STACK_OBJECT(obj->obj_field_acquire(field_offset), -1);
}else if (tos_type == itos) { // int 型的數(shù)據(jù)
SET_STACK_INT(obj->int_field_acquire(field_offset), -1);
}
//。。。。。。。。。。。
}
拿到這個值,去看這個 cache 是不是 volatile 修飾的。
oop.inline.hpp
static void release_store(volatile jint* P, jint v);
根據(jù)不同的操作系統(tǒng),有不同的實現(xiàn)。 JMM 是為了解決不同的系統(tǒng)做的處理方案。
inline void OrderAccess::release sotre(volatile jint* p, jint v){*p = v;} // 語言級別的內(nèi)存屏障
volatile 是一個語言級別的 memery barry
被 `volatile` 聲明的變量隨時都可能發(fā)生變化,每次使用的時候,必須從這個變量的對應(yīng)的內(nèi)存地址去讀取,編譯器對這個 `volatile` 修飾的變量不會去做代碼優(yōu)化。
內(nèi)存屏障提供的幾種功能?
- 確保指令重排序,不會把它后邊指令排序到內(nèi)存屏障的前邊,也不會把內(nèi)存屏障前邊的指令排序到內(nèi)存屏障的后邊
- 強制對緩存的修改立即寫入到主內(nèi)存里邊。
- 如果是寫操作的話,會導(dǎo)致我們其他 CPU 的緩存無效。
規(guī)則
- 對每個
volatile寫操作的前邊會插入 storestore barrier - 對每個
volatile寫操作的后邊會插入 storeload barrier - 對每個
volatile讀操作前邊插入 loadload barrier - 對每個
volatile讀操作后邊插入 loadstore barrier
orderaccess_linux_x86.hpp
inline void OrderAccess::loadload() { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore() { compiler_barrier(); }
inline void OrderAccess::storeload() { fence(); }
假如說 是 storeload() 然后,調(diào)用 fence() 方法,
inline void OrderAccess::fence() {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
compiler_barrier();
}
匯編指令。 就是內(nèi)存屏障。storeload 就是一個內(nèi)存屏障。

OrderAccess::storeload();
是永遠追加在后邊的。是為了避免 `volatile` 寫操作后邊,有一些 `volatile` 讀寫操作的重排序。因為編譯器,沒辦法去判斷,`volatile` 后邊是不是還要去插入。為了保證正確實現(xiàn) `volatile` 語義,實現(xiàn)了悲觀策略。我最終都要加上 這個屏障。
public class VolatileDemo1 {
private static volatile boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int i = 9;
while (!stop){
i++;
}
});
thread.start();
Thread.sleep(1000);
stop = true;
}
}
對于 `volatile` 修飾的變量,如果其他的線程對這個值進行了一個變更的話,他會去加一個內(nèi)存屏障,他會去保證我們的可見性。我們會保證在我們的CPU 層面,就是我們的匯編指令層面,它實際上會去發(fā)起一個 LOCK 的匯編指令,這個 LOCK 指令最終做的就是把我們的這個緩存更新到我們的內(nèi)存里邊。
原子性
對符合操作的原子性是沒有辦法保證原子性的?。?!
public class VolatileIncrDemo {
volatile int i = 0;
public void incr() {
i++;
}
public static void main(String[] args) {
new VolatileIncrDemo().incr();
}
}
javap -c volatileIncrDemo.class 之后
public void incr();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
對一個原子遞增的操作,會分為三個步驟:
- 讀取 volatile 變量的值到 local ;
- 增加變量的值;
- 把 local 的值回寫
多個線程同時去執(zhí)行的話。三個操作。
每個線程可能拿到舊的值去更新。
Synchronized 原子性,避免線程的并行執(zhí)行
AtomicInteger ( CAS ) 、Lock ( CAS/ LockSupport / AQS / unsafe )
不安全都放到 unsafe ,一般不推薦使用。
Synchronized
- 可見性
- 原子性
- 有序性
總結(jié)
內(nèi)存模型
- 約束我們線程訪問內(nèi)存的規(guī)范。
- 屏蔽硬件和操作系統(tǒng)的內(nèi)存訪問的差異。
JMM 對硬件和操作系統(tǒng)的抽象。定義了,線程之間可以通過共享空間和線程信號去通訊。
volatile 通過 LOCK 來實現(xiàn)鎖。
- 編譯器的指令重排序
- CPU 的指令重排序(內(nèi)存的亂序訪問)
可見性問題
內(nèi)存屏障去解決。
int a = 1;
int b = b ;
a = 1 ; storestore ; b = 2 ; a = 2
Volatile 是干嘛的?
- 可以保證可見性、防止內(nèi)存重排序
-
#LOCK, - > 緩存鎖 (MESI) - 內(nèi)存屏障
使用場景
線程的關(guān)閉。
java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;
成員變量 state 的定義。

