Java解決線程的不安全問題之volatile關(guān)鍵字詳解
1. 造成線程不安全的代碼
有一代碼,要求兩個線程運行。
并自定義一個標志位 flag,當線程2(thread2)修改標志位后,線程1(thread1)結(jié)束執(zhí)行。
如下代碼所示:
public class TestDemo3 {
public static int flag = 0;//自定義一個標志位
public static void main(String[] args) {
Thread thread1 = new Thread(()-> {
while (flag == 0) {
//空
}
System.out.println("thread1線程結(jié)束");
});//線程1
Thread thread2 = new Thread(()-> {
Scanner scanner = new Scanner(System.in);
System.out.println("請輸入一個整數(shù):");
flag = scanner.nextInt();
});//線程2
thread1.start();//啟動線程1
thread2.start();//啟動線程2
}
}運行后打印:

預(yù)期效果為:thread1 中的 flag==0 作為條件進入 while 循序,thread2 中通過 scanner 輸入一個非 0 的值,從而使得 thread1 線程結(jié)束。
實際效果:thread2 中輸入非 0 數(shù)后,光標處于閃爍狀態(tài)代表循環(huán)未結(jié)束。
造成程序沒有達到如期效果的原因是內(nèi)存的不可見性導(dǎo)致 while 條件判斷始終發(fā)生錯誤。
因此,我們得使用 volatile 關(guān)鍵字來保證內(nèi)存的可見性,使得 while 條件判斷能夠正常識別修改后的標志位 flag。
2. volatile能保證內(nèi)存可見性
可見性指一個線程對共享變量值的修改,能夠及時地被其他線程看到。
而 volatile 關(guān)鍵字就保證內(nèi)存的可見性。
在上述代碼中標志位 flag 未使用 volatile 修飾導(dǎo)致 while 循環(huán)不能正確判斷,其原因如下:
flag == 0這個判斷,會實現(xiàn)兩條操作:
- 第一條,load 從內(nèi)存讀取數(shù)據(jù)到 cpu的 寄存器。
- 第二條,cmp 比較寄存器中的值是否為0,是則返回 true 否則返回 false。
但是,編譯器有一個特性:優(yōu)化。優(yōu)化什么呢?
由于進行大量數(shù)據(jù)操作時 load 的開銷很大,編譯器就做出了一個優(yōu)化,就是無論數(shù)據(jù)大或小 load 操作只會執(zhí)行一次。
因此,flag == 0 這個條件第一作為 load 加載到了寄存器中,后序無論對 flag 進行怎樣的修改 cmp 比較的時候始終為 true 了。

這就是多線程運行時,編譯器對于代碼進行優(yōu)化操作的內(nèi)存不可見性。也就是內(nèi)存看不到實際的情況。
因此,我們只需要在 flag 前面加上 volatile 關(guān)鍵字使得編譯器不對 flag 進行優(yōu)化,這樣就能達到效果。如下代碼所示:
public class TestDemo3 {
volatile public static int flag = 0;//volatile修飾自定義標志位
public static void main(String[] args) {
Thread thread1 = new Thread(()-> {
while (flag == 0) {
//空
}
System.out.println("thread1線程結(jié)束");
});//線程1
Thread thread2 = new Thread(()-> {
Scanner scanner = new Scanner(System.in);
System.out.println("請輸入一個整數(shù):");
flag = scanner.nextInt();
});//線程2
thread1.start();//啟動線程1
thread2.start();//啟動線程2
}
}運行后打?。?/p>

通過上述代碼及打印結(jié)果,可以看到達到了預(yù)期效果。因此,被 volatile 修飾的變量能夠保證每次從內(nèi)存中重新讀取數(shù)據(jù)。
解釋內(nèi)存可見性:
thread1頻繁讀取主內(nèi)存,效率比較第,就被優(yōu)化成直接讀直接的工作內(nèi)存
thread2修改了主內(nèi)存的結(jié)果,由于thread1沒有讀主內(nèi)存,導(dǎo)致修改不能被識別
上述的工作內(nèi)存理解為CPU寄存器,主內(nèi)存理解為內(nèi)存。
3. synchronized與volatile的區(qū)別
3.1 synchronized能保證原子性
以下代碼的需求為:兩個線程分別計算10000 次,使得 count 總數(shù)達到 20000:
//創(chuàng)建一個自定義類
class myThread {
int count = 0;
public void run() {
synchronized (this){
count++;
}
}
public int getCount() {
return count;
}
}
public class TreadDemo1 {
public static void main(String[] args) throws InterruptedException {
myThread myThread = new myThread();//實例化這個類
Thread thread1 = new Thread(()-> {
for (int i = 0; i < 10000; i++) {
myThread.run();
}
});
Thread thread2 = new Thread(()-> {
for (int i = 0; i < 10000; i++) {
myThread.run();
}
});
thread1.start();//啟動線程thread1
thread2.start();//啟動線程thread2
thread1.join();//等待線程thread1結(jié)束
thread2.join();//等待線程thread2結(jié)束
System.out.println(myThread.getCount());//獲取count值
}
}運行后打印:

3.2 volatile不能保證原子性
當我們把上述代碼中的 run 方法去掉 synchronized 的關(guān)鍵字,再給 count 變量加上 volatile 關(guān)鍵字。

//創(chuàng)建一個自定義類
class myThread {
volatile int count = 0;
public void run() {
count++;
}
public int getCount() {
return count;
}
}運行后打印:

到此這篇關(guān)于Java解決線程的不安全問題之volatile關(guān)鍵字詳解的文章就介紹到這了,更多相關(guān)Java的volatile關(guān)鍵字內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring boot 常見http請求url參數(shù)獲取方法
這篇文章主要介紹了spring boot 常見http請求url參數(shù)獲取,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03
實戰(zhàn)分布式醫(yī)療掛號系統(tǒng)開發(fā)醫(yī)院科室及排班的接口
這篇文章主要為大家介紹了實戰(zhàn)分布式醫(yī)療掛號系統(tǒng)開發(fā)醫(yī)院科室及排班的接口,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪<BR>2022-04-04

