線程間通信
線程間的通信一般有兩種方式進(jìn)行,一是通過消息傳遞
,二是共享內(nèi)存
。Java 線程間的通信采用的是共享內(nèi)存方式,JMM 為共享變量提供了線程間的保障。如果兩個(gè)線程都對一個(gè)共享變量進(jìn)行操作,共享變量初始值為 1,每個(gè)線程都變量進(jìn)行加 1,預(yù)期共享變量的值為 3。在 JMM 規(guī)范下會有一系列的操作。我們直接來看下圖:
在多線程的情況下,對主內(nèi)存中的共享變量進(jìn)行操作可能發(fā)生線程安全問題,比如:線程 1 和線程 2 同時(shí)對同一個(gè)共享變量進(jìn)行操作,執(zhí)行+1
操作,線程 1 、線程2 讀取的共享變量是否是彼此修改前還是修改后的值呢,這個(gè)是無法確定的,這種情況和CPU的高速緩存與內(nèi)存之間的問題非常相似
如何實(shí)現(xiàn)主內(nèi)存與工作內(nèi)存的變量同步,為了更好的控制主內(nèi)存和本地內(nèi)存的交互,Java 內(nèi)存模型定義了八種操作來實(shí)現(xiàn):
- lock:鎖定。作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識為一條線程獨(dú)占狀態(tài)。
- unlock:解鎖。作用于主內(nèi)存變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
- read:讀取。作用于主內(nèi)存變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用
- load:載入。作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
工作內(nèi)存即本地內(nèi)存
。 - use:使用。作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會執(zhí)行這個(gè)操作。
- assign:賦值。作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
- store:存儲。作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
- write:寫入。作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中。
重溫Java 并發(fā)三大特性
原子性
原子性:即一個(gè)或者多個(gè)操作作為一個(gè)整體,要么全部執(zhí)行,要么都不執(zhí)行,并且操作在執(zhí)行過程中不會被線程調(diào)度機(jī)制打斷;而且這種操作一旦開始,就一直運(yùn)行到結(jié)束,中間不會有任何上下文切換(context switch) 比如:
int i = 0; //語句1,原子性
i++; //語句2,非原子性
語句1大家一幕了然,語句2卻許多人容易犯迷糊,i++
其實(shí)可以分為3步:
- i 被從局部變量表(內(nèi)存)取出,
- 壓入操作棧(寄存器),操作棧中自增
- 使用棧頂值更新局部變量表(寄存器更新寫入內(nèi)存)
執(zhí)行上述3個(gè)步驟的時(shí)候是可以進(jìn)行線程切換的,或者說是可以被另其他線程的 這3 步打斷的,因此語句2
不是一個(gè)原子性操作
在 Java 中,可以借助synchronized
、各種 Lock
以及各種原子類實(shí)現(xiàn)原子性。synchronized
和各種Lock
是通過保證任一時(shí)刻只有一個(gè)線程訪問該代碼塊,因此可以保證其原子性。各種原子類是利用CAS (compare and swap)
操作(可能也會用到 volatile
或者final
關(guān)鍵字)來保證原子操作。
可見性
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看到修改的值。我們來看一個(gè)例子:
public class VisibilityTest {
private boolean flag = true;
public void change() {
flag = false;
System.out.println(Thread.currentThread().getName() + ",已修改flag=false");
}
public void load() {
System.out.println(Thread.currentThread().getName() + ",開始執(zhí)行.....");
int i = 0;
while (flag) {
i++;
}
System.out.println(Thread.currentThread().getName() + ",結(jié)束循環(huán)");
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 線程threadA模擬數(shù)據(jù)加載場景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 讓threadA執(zhí)行一會兒
Thread.sleep(1000);
// 線程threadB 修改 共享變量flag
Thread threadB = new Thread(() -> test.change(), "threadB");
threadB.start();
}
}
threadA 負(fù)責(zé)循環(huán),threadB負(fù)責(zé)修改 共享變量flag
,如果flag=false時(shí),threadA 會結(jié)束循環(huán),但是上面的例子會死循環(huán)。原因是threadA無法立即讀取到共享變量flag修改后的值。我們只需 private volatile boolean flag = true;
加上volatile
關(guān)鍵字threadA就可以立即退出循環(huán)了。
Java中的volatile關(guān)鍵字
提供了一個(gè)功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次是用之前都從主內(nèi)存刷新。
因此,可以使用volatile
來保證多線程操作時(shí)變量的可見性。除了volatile
,Java中的synchronized
和final
兩個(gè)關(guān)鍵字 以及各種 Lock也可以實(shí)現(xiàn)可見性。
有序性
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
int i = 0;
int j = 0;
i = 10; //語句1
j = 1; //語句2
但由于指令重排序問題,代碼的執(zhí)行順序未必就是編寫代碼時(shí)候的順序。語句可能的執(zhí)行順序如下:
- 語句1 語句2
- 語句2 語句1
指令重排對于非原子性的操作,在不影響最終結(jié)果的情況下,其拆分成的原子操作可能會被重新排列執(zhí)行順序。 指令重排不會影響單線程的執(zhí)行結(jié)果,但是會影響多線程并發(fā)執(zhí)行的結(jié)果正確性 。在Java 中,可以通過volatile關(guān)鍵字
來禁止指令進(jìn)行重排序優(yōu)化,詳情可見:https://mp.weixin.qq.com/s/TyiCfVMeeDwa-2hd9N9XJQ。也可以使用synchronized關(guān)鍵字
保證同一時(shí)刻只允許一條線程訪問程序塊。
參考資料:
《java并發(fā)編程實(shí)戰(zhàn)》
https://www.cnblogs.com/czwbig/p/11127124.html
https://www.cnblogs.com/jelly12345/p/14609657.html
https://www.cnblogs.com/bailiyi/p/11967396.html
-
JAVA
+關(guān)注
關(guān)注
19文章
2952瀏覽量
104489 -
編譯器
+關(guān)注
關(guān)注
1文章
1617瀏覽量
49017 -
JVM
+關(guān)注
關(guān)注
0文章
157瀏覽量
12197
發(fā)布評論請先 登錄
相關(guān)推薦
評論