Java中線程狀態(tài)+線程安全問題+synchronized的用法詳解
java中的線程狀態(tài)??
在操作系統(tǒng)層面,一個(gè)線程就兩個(gè)狀態(tài):就緒和阻塞狀態(tài).
但是java中為了在線程阻塞時(shí)能夠更快速的知曉一個(gè)線程阻塞的原因,又將阻塞的狀態(tài)進(jìn)行了細(xì)化.
- NEW:線程對(duì)象已經(jīng)創(chuàng)建好了,但是系統(tǒng)層面的線程還沒創(chuàng)建好,或者說線程對(duì)象還沒調(diào)用start()
- TERMINATED:系統(tǒng)中的線程已經(jīng)銷毀,但是代碼中的線程對(duì)象還在,也就是run()跑完了,Thread對(duì)象還在
- RUNNABLE:線程位于就緒隊(duì)列,隨時(shí)都有可能被cpu調(diào)度執(zhí)行
- TIMED_WAITING:線程執(zhí)行過程中,線程對(duì)象調(diào)用了sleep(),進(jìn)入阻塞,休眠時(shí)間到了,就會(huì)回到就緒隊(duì)列
- BLOCKED:有一個(gè)線程將一個(gè)對(duì)象上鎖(synchronized)之后,另一個(gè)線程也想給這個(gè)對(duì)象上鎖,就會(huì)陷入BLOCKED狀態(tài),只有第一個(gè)線程將鎖對(duì)象解鎖了,后一個(gè)線程才有可能給這個(gè)對(duì)象進(jìn)行上鎖.
- WAITING:搭配synchronized進(jìn)行使用wait(),一旦一個(gè)線程調(diào)用了wait(),會(huì)先將所對(duì)象解鎖,等到另一個(gè)線程進(jìn)行notify(),之后wait中的線程才會(huì)被喚醒,當(dāng)然也可以在wait()中設(shè)置一個(gè)最長(zhǎng)等待時(shí)間,防止出現(xiàn)死等.
線程安全問題案例分析??
多線程對(duì)同一變量進(jìn)行寫操作??
- 概念:一串代碼什么時(shí)候叫作有線程安全問題呢?首先線程安全問題的罪惡之源是,多線程并發(fā)執(zhí)行的時(shí)候,會(huì)有搶占式執(zhí)行的現(xiàn)象,這里的搶占式執(zhí)行,執(zhí)行的是機(jī)器指令!那一串代碼什么時(shí)候叫作有線程安全問題呢?多線程并發(fā)時(shí),不管若干個(gè)線程怎么去搶占式執(zhí)行他們的代碼,都不會(huì)影響最終結(jié)果,就叫作線程安全,但是由于搶占式執(zhí)行,出現(xiàn)了和預(yù)期不一樣的結(jié)果,就叫作有線程安全問題,出bug了!
- 典型案例:使用兩個(gè)線程對(duì)同一個(gè)數(shù)進(jìn)行自增操作10w次:
public class Demo1 { private static int count=0; public static void main(String[] args) { Thread t1=new Thread(()->{ for(int i=0;i<50000;i++){ count++; } }); t1.start(); Thread t2=new Thread(()->{ t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(count); } } //打印結(jié)果:68994
顯然預(yù)期結(jié)果是10w,但算出來就是6w多,這就是出現(xiàn)了線程安全問題.
分析原因:
僅針對(duì)每個(gè)線程的堆count進(jìn)行自增的操作:首先要明白,進(jìn)行一次自增的機(jī)器指令有三步:從主內(nèi)存中把count值拿到cpu寄存器中->把寄存器中的count值進(jìn)行自增1->把寄存器中的count值刷新到主內(nèi)存中,我們姑且把這三步叫作:load->add->save
我們假設(shè)就是在一個(gè)cpu上(畫兩個(gè)cpu好表示)并發(fā)執(zhí)行兩組指令(就不會(huì)出現(xiàn)同時(shí)load這樣的情況了):
如出現(xiàn)上圖的情況:
觀察發(fā)現(xiàn):兩個(gè)線程都是執(zhí)行了一次count++,但是兩次++的結(jié)果卻不如意,相當(dāng)于只進(jìn)行了一次自增,上述就是出現(xiàn)了線程安全問題了.
并且我們可以預(yù)測(cè)出上述代碼的結(jié)果范圍:5w-10w之間!,為什么呢?
上面兩張圖表示的是出現(xiàn)線程安全問題的情況,表現(xiàn)的結(jié)果就是兩次加加當(dāng)一次去用了,如果兩個(gè)線程一直處于這樣的狀態(tài)(也是最壞的狀態(tài)了),可不就是計(jì)算結(jié)果就是5w咯,那如果兩個(gè)線程一直是一個(gè)線程完整的執(zhí)行完load-add-save之后,另一個(gè)線程再去執(zhí)行這樣的操作,那就串行式執(zhí)行了,可不就是10w咯.
3.針對(duì)上述案例如何去解決呢?
案例最后也提到了,只要能夠?qū)崿F(xiàn)串行式執(zhí)行,就能保證結(jié)果的正確性,那java確實(shí)有這樣的功能供我們使用,即synchronized關(guān)鍵字的使用.
也就是說:cpu1執(zhí)行l(wèi)oad之前先給鎖對(duì)象進(jìn)行加鎖,save之后再進(jìn)行解鎖,cpu2此時(shí)才能去給那個(gè)對(duì)象進(jìn)行上鎖,并進(jìn)行一系列的操作.此時(shí)也就是保證了load-add-save的原子性,使得這三個(gè)步驟要么就別執(zhí)行,執(zhí)行就一口氣執(zhí)行完.
那你可能會(huì)提問,那這樣和只用一個(gè)main線程去計(jì)算自增10w次有什么區(qū)別,創(chuàng)建多線程還有什么意義呢?
意義很大,因?yàn)槲覀儎?chuàng)建的線程很多時(shí)候不僅僅只是一個(gè)操作,光針對(duì)自增我們可以通過加鎖防止出現(xiàn)線程安全問題,但是各線程的其他操作要是不涉及線程安全問題那就可以并發(fā)了呀,那此時(shí)不就大大提升了執(zhí)行效率咯.
4.具體如何加鎖呢?
此處先只說一種加鎖方式,先把上述案例的問題給解決了再說.
使用關(guān)鍵字synchronized,此處使用的是給普通方法加synchronized修飾的方法(除此之外,synchronized還可以修飾代碼塊和靜態(tài)方法)
class Counter{ private int count; synchronized public void increase(){ this.count++; } public int getCount(){ return this.count; } } public class Demo2 { private static int num=50000; public static void main(String[] args) { Counter counter=new Counter();//此時(shí)對(duì)象中的count值默認(rèn)就是0 Thread t1=new Thread(()->{ for (int i = 0; i < num; i++) { counter.increase(); } }); t1.start(); Thread t2=new Thread(()->{ for (int i = 0; i < num; i++) { counter.increase(); } }); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter.getCount()); } }//打印10W
內(nèi)存可見性問題??
首先說明:這是有編譯器優(yōu)化導(dǎo)致的,其次要知道cpu讀取變量時(shí):先從主內(nèi)存將變量的值存至緩存或者寄存器中,cpu計(jì)算時(shí)再在寄存器中讀取這個(gè)值.
當(dāng)某線程頻繁的從內(nèi)存中讀取一個(gè)不變的變量時(shí),編譯器將會(huì)把從內(nèi)存獲取變量的值直接優(yōu)化成從寄存器直接獲取.之所以這樣優(yōu)化,是因?yàn)?cpu從主內(nèi)存中讀取一個(gè)變量比在緩存或者寄存器中讀取一個(gè)變量的值慢成千上萬倍,如果每每在內(nèi)存中讀到的都是同一個(gè)值,既然緩存里頭已經(jīng)有這個(gè)值了,干嘛還大費(fèi)周折再去主內(nèi)存中進(jìn)行獲取呢,直接從緩存中直接讀取就可以了,可提升效率.
但是:一旦一個(gè)線程被優(yōu)化成上述的情況,那如果有另一個(gè)線程把內(nèi)存中的值修改了,我被優(yōu)化的線程還傻乎乎的手里拿著修改之前的值呢,或者內(nèi)存中的變量值被修改了,被優(yōu)化的線程此時(shí)已經(jīng)感應(yīng)不到了.
具體而言:
public class Demo3 { private static boolean flag=false; public static void main(String[] args) { Thread t1=new Thread(()->{ while(!flag){ System.out.println("我是優(yōu)化完之后直接讀取寄存器中的變量值才打印的哦!"); } }); t1.start(); flag=true; System.out.println("我已經(jīng)在主線程中修改了標(biāo)志位"); } }
運(yùn)行上述代碼之后,程序并不會(huì)終止,而是一直在那打印t1線程中的打印語(yǔ)句.
如何解決上述問題:
引入關(guān)鍵字volatile:防止內(nèi)存可見性問題,修飾一個(gè)變量,那某線程想獲取該變量的值的時(shí)候,只能去主內(nèi)存中獲取,其次它還可以防止指令重排序,指令重排問題會(huì)在線程安全的單例模式(懶漢)進(jìn)行介紹.具體:
public class Demo3 { private static volatile boolean flag=false; public static void main(String[] args) { Thread t1=new Thread(()->{ while(!flag){ System.out.println("我是優(yōu)化完之后直接讀取寄存器中的變量值才打印的哦!"); } }); t1.start(); try { Thread.sleep(1);//主線程給t1留有充足的時(shí)間先跑起來 } catch (InterruptedException e) { e.printStackTrace(); } flag=true; System.out.println("我已經(jīng)在主線程中修改了標(biāo)志位"); } } //打印若干t1中的打印語(yǔ)句之后,主線程main中修改標(biāo)志位之后,可以終止t1
注意:上述優(yōu)化現(xiàn)象只會(huì)出現(xiàn)在頻繁讀的情況,如果不是頻繁讀,就不會(huì)出現(xiàn)那樣的優(yōu)化.
指令重排序問題??
生活案例:買菜
如果是傻乎乎的按照菜單從上到下的去買菜,從路線圖可以看出,不必要的路是真的沒少走.
如果執(zhí)行代碼時(shí),編譯器認(rèn)為某些個(gè)代碼調(diào)整一下順序并不會(huì)影響結(jié)果,那代碼的執(zhí)行順序就會(huì)被調(diào)整,就比如可以把上面買菜的順序調(diào)整成:黃瓜->蘿卜->青菜->茄子
單線程這樣的指令重排一般不會(huì)出現(xiàn)問題,但是多線程并發(fā)時(shí),還這樣優(yōu)化,就容易出現(xiàn)問題
針對(duì)這樣的問題,如果是針對(duì)一個(gè)變量,我們可以使用volatile修飾,如果是針對(duì)代碼塊,我們可以使用synchronized.
synchronized的用法??
- synchronized起作用的本質(zhì)
- 修飾普通方法
- 修飾靜態(tài)方法
- 修飾代碼塊
synchronized起作用的本質(zhì)??
因?yàn)槲覀冎纉ava中所有類都繼承了Object,所以所有類都包含了Object的部分,我們可以稱這繼承的部分是"對(duì)象頭",使用synchronized進(jìn)行對(duì)象頭中的標(biāo)志位的修改,就可以做到一個(gè)對(duì)象的鎖一個(gè)時(shí)刻只能被一個(gè)線程所持有,其他線程此時(shí)不可搶占.這樣的設(shè)置,就好像把一個(gè)對(duì)象給鎖住了一樣.
修飾普通方法??
如前述兩個(gè)線程給同一個(gè)count進(jìn)行自增的案例.不再贅述.此時(shí)的所對(duì)象就是Counter對(duì)象
修飾靜態(tài)方法??
與普通方法類似.只不過這個(gè)方法可以類名直接調(diào)用.
修飾代碼塊??
首先修飾代碼塊需要執(zhí)行鎖對(duì)象是誰(shuí),所以這里可以分為三類,一個(gè)是修飾普通方法的方法體這個(gè)代碼塊的寫法,其次是修飾靜態(tài)方法方法體的寫法,最后可以單獨(dú)寫一個(gè)Object的對(duì)象,來對(duì)這個(gè)Object對(duì)象進(jìn)行上鎖.
class Counter{ private int count; public void increase(){ synchronized(this){ count++; } } public int getCount(){ return this.count; } }
class Counter{ private static int count; public static void increase(){ synchronized(Counter.class){//注意這里鎖的是類對(duì)象哦 count++; } } public int getCount(){ return this.count; } }
class Counter{ private static int count; private static Object locker=new Object(); public static void increase(){ synchronized(locker){ count++; } } public int getCount(){ return this.count; } }
注意:java中這種隨手拿一個(gè)對(duì)象就能上鎖的用法,是java中一種很有特色的用法,在別的語(yǔ)言中,都是有專門的鎖對(duì)象的.
Conclusion??
java中的線程狀態(tài),以及如何區(qū)分線程安全問題 罪惡之源是搶占式執(zhí)行多線程對(duì)同一個(gè)變量進(jìn)行修改,多線程只讀一個(gè)變量是沒有線程安全問題的修改操作是非原子性的內(nèi)存可見性引起的線程安全問題指令重排序引起的線程安全問題 synchronized的本質(zhì)和用法
1.java中的線程狀態(tài),以及如何區(qū)分
2.線程安全問題
- 罪惡之源是搶占式執(zhí)行
- 多線程對(duì)同一個(gè)變量進(jìn)行修改,多線程只讀一個(gè)變量是沒有線程安全問題的
- 修改操作是非原子性的
- 內(nèi)存可見性引起的線程安全問題
- 指令重排序引起的線程安全問題
3.synchronized的本質(zhì)和用法
到此這篇關(guān)于Java中線程狀態(tài)+線程安全問題+synchronized的用法詳解的文章就介紹到這了,更多相關(guān)java線程安全synchronized用法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Triple協(xié)議支持Java異?;貍髟O(shè)計(jì)實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了Triple協(xié)議支持Java異常回傳設(shè)計(jì)實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12使用idea的database模塊繪制數(shù)據(jù)庫(kù)er圖的方法
這篇文章主要介紹了使用idea的database模塊繪制數(shù)據(jù)庫(kù)er圖,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-07-07java中同類對(duì)象之間的compareTo()和compare()方法對(duì)比分析
這篇文章主要介紹了java中同類對(duì)象之間的compareTo()和compare()方法對(duì)比分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09Java基礎(chǔ)篇之serialVersionUID用法及注意事項(xiàng)詳解
這篇文章主要給大家介紹了關(guān)于Java基礎(chǔ)篇之serialVersionUID用法及注意事項(xiàng)的相關(guān)資料,SerialVersionUID屬性是用于序列化/反序列化可序列化類的對(duì)象的標(biāo)識(shí)符,我們可以用它來記住可序列化類的版本,以驗(yàn)證加載的類和序列化對(duì)象是否兼容,需要的朋友可以參考下2024-02-02如何使用BigDecimal實(shí)現(xiàn)Java開發(fā)商業(yè)計(jì)算
這篇文章主要介紹了如何使用BigDecimal實(shí)現(xiàn)Java開發(fā)商業(yè)計(jì)算,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09Java?Stream對(duì)象并行處理方法parallel()代碼示例
在Java中Stream是一種用于處理集合數(shù)據(jù)的流式操作API,它提供了一種簡(jiǎn)潔、靈活、高效的方式來對(duì)集合進(jìn)行各種操作,下面這篇文章主要給大家介紹了關(guān)于Java?Stream對(duì)象并行處理方法parallel()的相關(guān)資料,需要的朋友可以參考下2023-11-11java數(shù)據(jù)結(jié)構(gòu)之實(shí)現(xiàn)雙向鏈表的示例
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)雙向鏈表的示例,需要的朋友可以參考下2014-03-03mybatis配置文件簡(jiǎn)介_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了mybatis配置文件簡(jiǎn)介的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09