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