一文搞懂Java中的線程安全與線程同步
1.為什么需要線程同步
什么是線程安全:指在被多個(gè)線程訪問(wèn)時(shí),程序可以持續(xù)進(jìn)行正確的處理。
線程安全問(wèn)題
案例:通過(guò)搶優(yōu)惠例子說(shuō)明線程安全問(wèn)題
public?class?Demo1?{ ????public?static?void?main(String[]?args)?{ ????????//?簡(jiǎn)單模擬20人搶優(yōu)惠 ????????for(int?i=0;i<20;i++){ ????????????new?Thread(new?ThreadDemo()).start(); ????????} ????} } //?前十位可以獲取優(yōu)惠,憑號(hào)碼兌換優(yōu)惠 class?ThreadDemo?implements?Runnable{ ????private?static?Integer?num?=?10; ????@Override ????public?void?run()?{ ????????try?{ ????????????Thread.sleep(10); ????????}?catch?(InterruptedException?e)?{ ????????????e.printStackTrace(); ????????} ????????if(num<=0){ ????????????System.out.println("已被搶完,下次再來(lái)"); ????????????return; ????????} ????????System.out.println(Thread.currentThread().getName()+"用戶搶到的號(hào)碼:"+num--); ????} }
執(zhí)行結(jié)果:出現(xiàn)的問(wèn)題
- 同一個(gè)優(yōu)惠號(hào)碼可能被多次獲取到;
- 優(yōu)惠號(hào)碼可能獲取到0 和負(fù)數(shù),類似超買超賣
并發(fā)訪問(wèn)線程不安全的共享變量時(shí),會(huì)出現(xiàn)如上的常見問(wèn)題。
避免問(wèn)題的產(chǎn)生
- 共享變量設(shè)計(jì)為不可變的(final)
- 編程過(guò)程不修改共享變量(不修改)
- 將對(duì)象設(shè)計(jì)為無(wú)狀態(tài)的(無(wú)狀態(tài))
- 在修改共享變量時(shí)使用線程同步(通過(guò)鎖實(shí)現(xiàn))
2.怎么實(shí)現(xiàn)線程同步
線程同步是指程序中用于控制不同線程間操作發(fā)生相對(duì)順序的機(jī)制。
2.1.使用volatile關(guān)鍵字
volatile是輕量級(jí)的synchronized,對(duì)于共享變量可以通過(guò)volatile關(guān)鍵字來(lái)實(shí)現(xiàn)線程同步。
共享變量處理情況:總線先從主內(nèi)存拷貝共享變量到線程私有的工作內(nèi)存,再交由處理器進(jìn)行處理,處理完成后再?gòu)墓ぷ鲀?nèi)存將運(yùn)算結(jié)果回寫主內(nèi)存。工作內(nèi)存起到臨時(shí)緩存數(shù)據(jù)和指令的作用、并且線程私有,這就會(huì)存在緩存不一致問(wèn)題。
實(shí)現(xiàn)原理:有volatile變量修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)多出一行l(wèi)ock 前綴指令的匯編代碼,lock前綴指令會(huì)直接鎖緩存行,起到內(nèi)存屏障的效果,并使處理器立即執(zhí)行緩存回寫到主內(nèi)存的操作,并且導(dǎo)致其他處理器的緩存失效,需要重新從主內(nèi)存獲取最新值。
附上一張圖便于理解
Java線程需要由操作系統(tǒng)內(nèi)核線程調(diào)度器進(jìn)行調(diào)度,并不是直接訪問(wèn)處理器資源,圖中僅展示關(guān)鍵的幾個(gè)點(diǎn)。
怎么輸出匯編指令
windows:下載hsdis-amd64.dll 提取碼:w5a2,并放在目錄 jdk1.8.0_181\jre\bin\server
在idea 工具中配置VM:-Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*TestVolatile.main
//?測(cè)試代碼 public?class?TestVolatile?{ ????private?static?volatile?int?num?=?0; ????public?static?void?main(String[]?args)?{ ????????num--; ????????System.out.println("結(jié)果為:"?+?num); ????} }
執(zhí)行結(jié)果:lock add dword ptr [rsp]
0x000000000320606d: lock add dword ptr [rsp],0h ;*putstatic num ; - com.yty.concurrent.synchronizeddemo.TestVolatile::main@5 (line 7)
通過(guò)緩存鎖定和緩存一致性協(xié)議實(shí)現(xiàn)可見性,確保多線程程序讀寫共享變量的時(shí)候,每個(gè)CPU看到的都是最新值。
MESI 高速緩存一致性協(xié)議,處理器使用嗅探技術(shù)保證緩存、主內(nèi)存和其他處理器緩存的數(shù)據(jù)一致。還有其他的緩存一致性協(xié)議,比如:AMD的MOESI協(xié)議、Intel的MOSIF協(xié)議。
MESI 分別表示:
- M(Modify):修改
- E(Exclusive):獨(dú)占、互斥
- S(Shared):共享
- I(Invalid):無(wú)效
volatile 關(guān)鍵字的另一個(gè)作用是禁止編譯器或處理器對(duì)指令進(jìn)行重排序優(yōu)化。
什么是重排序:重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重新排序的一種手段。說(shuō)白了,如果在編譯或運(yùn)行期間發(fā)生重排序,那么程序就可能不按照編寫的順序執(zhí)行,會(huì)出現(xiàn)意料之外的執(zhí)行結(jié)果。
編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序,一般重排序可能發(fā)生在沒有數(shù)據(jù)依賴關(guān)系的指令之中。就算數(shù)據(jù)之間沒有依賴關(guān)系,在多線程場(chǎng)景中若發(fā)生指令重排序優(yōu)化,依然存在影響到最終執(zhí)行結(jié)果的情況。當(dāng)多線程場(chǎng)景下存在這種情況時(shí),就需要使用volatile關(guān)鍵字等其他手段去禁止編譯器和處理器對(duì)指令重排序優(yōu)化。
實(shí)現(xiàn)原理是在編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。
回來(lái)到一開始的案例問(wèn)題,給線程類的共享變量 private static Integer num = 10; 加上volatile關(guān)鍵字能實(shí)現(xiàn)線程安全嗎?
private?static?volatile?Integer?num?=?10;
答案是:不能
原因是:指令【num--】看似一條指令,其實(shí)分成三步執(zhí)行:先獲取、再計(jì)算、后保存,所以自減和自增都不是原子性操作,而volatile 無(wú)法確保其原子性操作,所以使用volatile關(guān)鍵字無(wú)法確保該情況下的線程安全,需要使用鎖來(lái)實(shí)現(xiàn)原子性操作。
2.2.使用synchronized關(guān)鍵字
synchronized塊是Java提供的一種原子性內(nèi)置鎖,Java中的每個(gè)對(duì)象都可以把它當(dāng)作一個(gè)同步鎖來(lái)使用,也稱為監(jiān)視器鎖。synchronized 同步代碼塊中的操作被看為原子性操作。同步鎖是一種排它鎖、獨(dú)占鎖、可重入鎖,當(dāng)一個(gè)線程獲取這個(gè)鎖后,其他線程必須等待該線程釋放鎖后才能獲取該鎖,并支持鎖釋放后可以再次獲取鎖。
使用方式
private?Integer?num?=?1; private?static?Integer?num2?=?1; public?synchronized?void?test1(){ ????System.out.println("普通方法"?+?num++); } public?static?synchronized?void?test2(){ ????System.out.println("靜態(tài)方法"?+?num2++); } public?void?test1_1(){ ????synchronized?(this){ ????????System.out.println("鎖當(dāng)前對(duì)象"+?num++); ????} ????synchronized?(TestSynchronized.class){ ????????System.out.println("鎖當(dāng)前類"?+?num++); ????} ????synchronized?(num){ ????????System.out.println("鎖指定變量"?+?num++); ????} }
synchronized 關(guān)鍵字加在普通方法時(shí),是給當(dāng)前實(shí)例對(duì)象上鎖;
synchronized 關(guān)鍵字加在靜態(tài)方法時(shí),是給當(dāng)前Class類對(duì)象上鎖;
synchronized 關(guān)鍵字加在同步代碼塊時(shí),是給括號(hào)里配置的對(duì)象上鎖。
注意:這個(gè)Class類對(duì)象指的是每個(gè)類在類加載過(guò)程中生成的Class類。
原理簡(jiǎn)析
在synchronized的同步代碼塊中,入口處執(zhí)行了monitorenter,在其出口執(zhí)行了monitorexit,虛擬機(jī)中的每個(gè)Object實(shí)例都有一個(gè)monitor(監(jiān)視器鎖);而同步方法通過(guò)字節(jié)碼flags 標(biāo)記該方法為ACC_SYNCHRONIZED,表明執(zhí)行該方法時(shí)需要獲取到監(jiān)視器鎖才可以執(zhí)行。兩者的本質(zhì)都是對(duì)一個(gè)對(duì)象的監(jiān)視器(monitor)的獲取和釋放。
可以使用javap -c -v xxx.class
查看字節(jié)碼信息
同步代碼塊
同步方法
關(guān)于一開始的例子,可以寫成
public?class?Demo1?{ ????public?static?void?main(String[]?args)?{ ????????//?簡(jiǎn)單模擬20人搶優(yōu)惠 ????????for(int?i=0;i<20;i++){ ????????????new?Thread(new?ThreadDemo()).start(); ????????} ????} } //?前十位可以獲取優(yōu)惠,憑號(hào)碼兌換優(yōu)惠 class?ThreadDemo?implements?Runnable{ ????private?static?Integer?num?=?10; ????@Override ????public?void?run()?{ ????????try?{ ????????????Thread.sleep(10); ????????}?catch?(InterruptedException?e)?{ ????????????e.printStackTrace(); ????????} ????????synchronized?(num){ ????????????if(num<=0){ ????????????????System.out.println("已被搶完,下次再來(lái)"); ????????????????return; ????????????} ????????????System.out.println(Thread.currentThread().getName()+"用戶搶到的號(hào)碼:"+num--); ????????} ????} }
或者改為
synchronized?(ThreadDemo.class){ ????if(num<=0){ ????????System.out.println("已被搶完,下次再來(lái)"); ????????return; ????} ????System.out.println(Thread.currentThread().getName()+"用戶搶到的號(hào)碼:"+num--); }
注意:num 變量是static變量,是屬于類的變量,需要鎖住變量對(duì)象或Class類,單獨(dú)或組合使用synchronized (this) 和volatile都無(wú)法確保其線程安全,這個(gè)可自行驗(yàn)證。
案例:對(duì)象單例實(shí)現(xiàn)
public?class?SingletonDemo?{ ????private?static?volatile?SingletonDemo?singletonDemo; ????public?SingletonDemo(){ ????} ????public?static?SingletonDemo?getInstance(){ ????????if(singletonDemo==null) ????????????synchronized?(SingletonDemo.class){ ????????????????if?(singletonDemo==null) ????????????????????singletonDemo?=?new?SingletonDemo(); ????????????} ????????return?singletonDemo; ????} } //?測(cè)試 class?Demo{ ????public?static?void?main(String[]?args)?{ ????????Demo?demo?=?new?Demo(); ????????for(int?i=0;i<10000;i++){ ????????????demo.test(); ????????} ????} ????public?void?test(){ ????????new?Thread(()->{ ????????????SingletonDemo?instance?=?SingletonDemo.getInstance(); ????????????System.out.println(Thread.currentThread().getName()+"="+instance); ????????}).start(); ????} }
說(shuō)明:volatile 和 synchronized實(shí)現(xiàn)雙重鎖校驗(yàn)。synchronized 起到指令執(zhí)行的原子性和同一時(shí)間只能單個(gè)線程執(zhí)行;synchronized和volatile都起到內(nèi)存可見性保證;volatile起到禁止指令重排序,指令重排序?qū)е戮€程不安全的可能性較小,但存在可能發(fā)生。
除了簡(jiǎn)單易用的synchronized 同步鎖之外,還有其他更靈活的鎖。
篇幅原因,將在下一篇講述關(guān)于Java中的鎖:
以上就是一文搞懂Java中的線程安全與線程同步的詳細(xì)內(nèi)容,更多關(guān)于Java線程安全 線程同步的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決feign接口返回泛型設(shè)置屬性為null的問(wèn)題
這篇文章主要介紹了解決feign接口返回泛型設(shè)置屬性為null的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06關(guān)于Java大整數(shù)運(yùn)算之BigInteger類
這篇文章主要介紹了關(guān)于Java大整數(shù)運(yùn)算之BigInteger類,BigInteger提供高精度整型數(shù)據(jù)類型及相關(guān)操作,所有操作中,都以二進(jìn)制補(bǔ)碼形式表示,需要的朋友可以參考下2023-05-05Spring cloud踩坑記錄之使用feignclient遠(yuǎn)程調(diào)用服務(wù)404的方法
這篇文章主要給大家介紹了關(guān)于Spring cloud踩坑記錄之使用feignclient遠(yuǎn)程調(diào)用服務(wù)404的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11Java數(shù)據(jù)結(jié)構(gòu)順序表的詳細(xì)講解
大家好,今天給大家?guī)?lái)的是順序表,我覺得順序表還是有比較難理解的地方的,于是我就把這一塊的內(nèi)容全部整理到了一起,希望能夠給剛剛進(jìn)行學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)的人帶來(lái)一些幫助,或者是已經(jīng)學(xué)過(guò)這塊的朋友們帶來(lái)更深的理解,我們現(xiàn)在就開始吧2022-05-05Java實(shí)戰(zhàn)之小蜜蜂擴(kuò)音器網(wǎng)上商城系統(tǒng)的實(shí)現(xiàn)
這篇文章主要介紹了如何利用Java實(shí)現(xiàn)簡(jiǎn)單的小蜜蜂擴(kuò)音器網(wǎng)上商城系統(tǒng),文中采用到的技術(shù)有JSP、Servlet?、JDBC、Ajax等,感興趣的可以動(dòng)手試一試2022-03-03String實(shí)例化及static final修飾符實(shí)現(xiàn)方法解析
這篇文章主要介紹了String實(shí)例化及static final修飾符實(shí)現(xiàn)方法解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09Java實(shí)現(xiàn)用戶不可重復(fù)登錄功能
這篇文章主要介紹了Java實(shí)現(xiàn)用戶不可重復(fù)登錄功能,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下2016-12-12StringUtils,CollectionUtils判斷為空的方法和原生代碼哪個(gè)效率最高
這篇文章主要介紹了StringUtils,CollectionUtils判斷為空的方法和原生代碼哪個(gè)效率最高,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02