Java解決線程的不安全問(wèn)題之volatile關(guān)鍵字詳解
1. 造成線程不安全的代碼
有一代碼,要求兩個(gè)線程運(yùn)行。
并自定義一個(gè)標(biāo)志位 flag,當(dāng)線程2(thread2)修改標(biāo)志位后,線程1(thread1)結(jié)束執(zhí)行。
如下代碼所示:
public class TestDemo3 {
public static int flag = 0;//自定義一個(gè)標(biāo)志位
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("請(qǐng)輸入一個(gè)整數(shù):");
flag = scanner.nextInt();
});//線程2
thread1.start();//啟動(dòng)線程1
thread2.start();//啟動(dòng)線程2
}
}運(yùn)行后打印:

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

這就是多線程運(yùn)行時(shí),編譯器對(duì)于代碼進(jìn)行優(yōu)化操作的內(nèi)存不可見(jiàn)性。也就是內(nèi)存看不到實(shí)際的情況。
因此,我們只需要在 flag 前面加上 volatile 關(guān)鍵字使得編譯器不對(duì) flag 進(jìn)行優(yōu)化,這樣就能達(dá)到效果。如下代碼所示:
public class TestDemo3 {
volatile public static int flag = 0;//volatile修飾自定義標(biāo)志位
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("請(qǐng)輸入一個(gè)整數(shù):");
flag = scanner.nextInt();
});//線程2
thread1.start();//啟動(dòng)線程1
thread2.start();//啟動(dòng)線程2
}
}運(yùn)行后打?。?/p>

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

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

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

到此這篇關(guān)于Java解決線程的不安全問(wèn)題之volatile關(guān)鍵字詳解的文章就介紹到這了,更多相關(guān)Java的volatile關(guān)鍵字內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring boot 常見(jiàn)http請(qǐng)求url參數(shù)獲取方法
這篇文章主要介紹了spring boot 常見(jiàn)http請(qǐng)求url參數(shù)獲取,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
Java基于對(duì)象流實(shí)現(xiàn)銀行系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java基于對(duì)象流實(shí)現(xiàn)銀行系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09
實(shí)戰(zhàn)分布式醫(yī)療掛號(hào)系統(tǒng)開(kāi)發(fā)醫(yī)院科室及排班的接口
這篇文章主要為大家介紹了實(shí)戰(zhàn)分布式醫(yī)療掛號(hào)系統(tǒng)開(kāi)發(fā)醫(yī)院科室及排班的接口,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2022-04-04
盤(pán)點(diǎn)Java中延時(shí)任務(wù)的多種實(shí)現(xiàn)方式
當(dāng)需要一個(gè)定時(shí)發(fā)布系統(tǒng)通告的功能,如何實(shí)現(xiàn)??當(dāng)支付超時(shí),訂單自動(dòng)取消,如何實(shí)現(xiàn)?其實(shí)這些問(wèn)題本質(zhì)都是延時(shí)任務(wù)的實(shí)現(xiàn),本文為大家盤(pán)點(diǎn)了多種常見(jiàn)的延時(shí)任務(wù)實(shí)現(xiàn)方法,希望對(duì)大家有所幫助2022-12-12
細(xì)數(shù)java for循環(huán)中的那些坑
這篇文章主要介紹了Java for循環(huán)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07
Java如何通過(guò)File類方法刪除指定文件夾中的全部文件
這篇文章主要給大家介紹了關(guān)于Java如何通過(guò)File類方法刪除指定文件夾中的全部文件的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01

