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)銷(xiāo)很大,編譯器就做出了一個(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è)自定義類(lèi) 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è)類(lèi) 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è)自定義類(lèi) 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-03Java基于對(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-07Java如何通過(guò)File類(lèi)方法刪除指定文件夾中的全部文件
這篇文章主要給大家介紹了關(guān)于Java如何通過(guò)File類(lèi)方法刪除指定文件夾中的全部文件的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01