併發編程系列之變量可見性問題探究
1、什麼是併發中的變量可見性問題
以例子的形式看看,定義一個變量,先用static修飾,在主線程修改之後,看看在新開的子線程裏能被看到?
public class Example {
private static boolean flag = true;
public void testss() {
new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (IfTest.flag) {
i++;
}
System.out.println(i);
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
IfTest.flag = false;
System.out.println("設置flag");
}
}
執行,控制檯打印:
設置flag
ps:主線程對flag變量進行修改,子線程是不能看到的,所以裏面一直在循環,不能打印統計數據值。然後怎麼才能讓併發線程看見?
- 方式1:使用volatile關鍵字
public class Example {
private static volatile boolean flag = true;
public void testss() {
new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (IfTest.flag) {
i++;
}
System.out.println(i);
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
IfTest.flag = false;
System.out.println("設置flag");
}
}
控制檯打印:
設置flag
72071943
- 方式2:使用synchronized同步鎖
public class Example {
private static boolean flag = true;
public void testss() {
new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (IfTest.flag) {
synchronized (this) {
i++;
}
}
System.out.println(i);
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
IfTest.flag = false;
System.out.println("設置flag");
}
}
控制檯打印:
設置flag
86726163
2、什麼是Java內存模型?
解答這個問題,需要涉及到Java的內存模型,如下所示,Java內存模型及操作規範:
- 共享變量都是放在主內存中的
- 每個線程都有自己的工作內存,線程只可操作自己的工作內存
- 線程要操作共享變量,需要從主內存中讀取到工作內存,改變值之後要從工作內存同步到主內存
- Java內存模型的同步交換協議,規定了8種原子操作
原子操作:不可被中斷的一個或一系列操作
- lock(鎖定):將主內存中的變量鎖定,爲一個線程所獨佔
- unlock(解鎖):將lock加的鎖解除,其他的線程有機會訪問此變量
- read(讀取):作用於主內存變量,將主內存中的變量值讀取到工作內存
- load(加載):作用於工作內存,將read讀取到的值保存到工作內存中的變量副本
- use(使用):作用於工作內存變量,將值傳遞給線程的代碼執行引擎
- assign(賦值):作用於工作內存變量,將執行引擎處理返回的值重新賦值給變量副本
- store(存儲):作用於工作內存變量,將變量副本的值傳送到主內存中
- write(寫入):作用於主內存變量,將store傳送過來的值寫入到主內存的共享變量中
- Java內存模型的同步交互協議,執行上述8種原子操作時必須滿足如下規則
- 不允許read和load,store和write操作之一單獨出現。即不允許加載或同步工作到一半。
- 不允許一個線程丟棄它最近的assign操作,即變量在工作內存中改變之後,必須將數據同步回主內存
- 不允許一個線程無原因地(無assign操作)將數據從工作內存同步到主內存中。
- 一個新的變量可能在主內存中誕生。
- 一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次lock之後必須要執行相同次數unlock操作,變量纔會解鎖
- 如果對一個對象進行lock操作,那麼會清空工作內存變量中的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始變量的值
- 如果一個對象事先沒有被lock,就不允許對其進行unlock操作,也不允許去unlock一個被其他線程鎖住的變量。
- 對一個變量執行unlock操作之前,必須將此變量同步回主內存中(執行store、write)
- Java內存模型的同步協議,操作規範
- 將一個變量從主內存複製到工作內存要順序執行read、load操作;要將變量從工作內存同步回主內存要用store、write操作。只要求順序執行,不一定是連續執行
圖引用網上資料:
3、保證變量可見性的方法
- final變量
- synchronized
- volatile修飾
4、Synchronized怎麼做到可見性
- synchronized語義規範:
- 進入同步塊前,先清空工作內存中的共享變量,從主內存加載
- 解鎖前,必須將修改的共享變量同步回主內存
- synchronized是如何做到線程安全的?
- 鎖機制保護共享資源,只有獲得鎖的線程才能操作共享資源
- synchronized語義規範保證了修改共享資源後,會同步回主內存,就做到了線程安全
5、volatile關鍵字解密
- volatile語義規範:
- 使用volatile變量時,必須重新從主內存加載到工作內存,並且read、load是連續的
- 修改volatile變量後,必須馬上同步回主內存,並且store、write是連續的
volatile可以做到線程安全?
不能,因爲volatile沒有鎖機制,線程是可以併發操作共享資源的volatile相對synchronized有什麼優點?
- 使用volatile比synchronized簡單
- volatile性能比synchronized好
- volatile的使用場景?
- volatile只能修飾成員變量
- 在多線程併發的場景才使用
- volatile支持併發編程三大特效?
併發編程三大特效:原子性、有序性、可見性。
- 可見性:volatile和synchronized關鍵字一樣,都可以保證可見性
- 有序性:volatile可以保證有序性,避免指令編排的情況,依賴於操作系統的內存屏障
- 原子行 :volatile只能保證單個操作的原子性,不能保證一系列操作的原子性,不能保證線程安全,所以說volatile不能保證原則性