您好,登錄后才能下訂單哦!
這篇文章主要介紹了Java中線程狀態+線程安全問題+synchronized的用法是什么的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Java中線程狀態+線程安全問題+synchronized的用法是什么文章都會有所收獲,下面我們一起來看看吧。
在操作系統層面,一個線程就兩個狀態:就緒和阻塞狀態.
但是java中為了在線程阻塞時能夠更快速的知曉一個線程阻塞的原因,又將阻塞的狀態進行了細化.
NEW:線程對象已經創建好了,但是系統層面的線程還沒創建好,或者說線程對象還沒調用start()
TERMINATED:系統中的線程已經銷毀,但是代碼中的線程對象還在,也就是run()跑完了,Thread對象還在
RUNNABLE:線程位于就緒隊列,隨時都有可能被cpu調度執行
TIMED_WAITING:線程執行過程中,線程對象調用了sleep(),進入阻塞,休眠時間到了,就會回到就緒隊列
BLOCKED:有一個線程將一個對象上鎖(synchronized)之后,另一個線程也想給這個對象上鎖,就會陷入BLOCKED狀態,只有第一個線程將鎖對象解鎖了,后一個線程才有可能給這個對象進行上鎖.
WAITING:搭配synchronized進行使用wait(),一旦一個線程調用了wait(),會先將所對象解鎖,等到另一個線程進行notify(),之后wait中的線程才會被喚醒,當然也可以在wait()中設置一個最長等待時間,防止出現死等.
概念:一串代碼什么時候叫作有線程安全問題呢?首先線程安全問題的罪惡之源是,多線程并發執行的時候,會有搶占式執行的現象,這里的搶占式執行,執行的是機器指令!那一串代碼什么時候叫作有線程安全問題呢?多線程并發時,不管若干個線程怎么去搶占式執行他們的代碼,都不會影響最終結果,就叫作線程安全,但是由于搶占式執行,出現了和預期不一樣的結果,就叫作有線程安全問題,出bug了!
典型案例:使用兩個線程對同一個數進行自增操作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); } } //打印結果:68994
顯然預期結果是10w,但算出來就是6w多,這就是出現了線程安全問題.
分析原因:
僅針對每個線程的堆count進行自增的操作:首先要明白,進行一次自增的機器指令有三步:從主內存中把count值拿到cpu寄存器中->把寄存器中的count值進行自增1->把寄存器中的count值刷新到主內存中,我們姑且把這三步叫作:load->add->save
我們假設就是在一個cpu上(畫兩個cpu好表示)并發執行兩組指令(就不會出現同時load這樣的情況了):
如出現上圖的情況:
觀察發現:兩個線程都是執行了一次count++,但是兩次++的結果卻不如意,相當于只進行了一次自增,上述就是出現了線程安全問題了.
并且我們可以預測出上述代碼的結果范圍:5w-10w之間!,為什么呢?
上面兩張圖表示的是出現線程安全問題的情況,表現的結果就是兩次加加當一次去用了,如果兩個線程一直處于這樣的狀態(也是最壞的狀態了),可不就是計算結果就是5w咯,那如果兩個線程一直是一個線程完整的執行完load-add-save之后,另一個線程再去執行這樣的操作,那就串行式執行了,可不就是10w咯.
3.針對上述案例如何去解決呢?
案例最后也提到了,只要能夠實現串行式執行,就能保證結果的正確性,那java確實有這樣的功能供我們使用,即synchronized關鍵字的使用.
也就是說:cpu1執行load之前先給鎖對象進行加鎖,save之后再進行解鎖,cpu2此時才能去給那個對象進行上鎖,并進行一系列的操作.此時也就是保證了load-add-save的原子性,使得這三個步驟要么就別執行,執行就一口氣執行完.
那你可能會提問,那這樣和只用一個main線程去計算自增10w次有什么區別,創建多線程還有什么意義呢?
意義很大,因為我們創建的線程很多時候不僅僅只是一個操作,光針對自增我們可以通過加鎖防止出現線程安全問題,但是各線程的其他操作要是不涉及線程安全問題那就可以并發了呀,那此時不就大大提升了執行效率咯.
4.具體如何加鎖呢?
此處先只說一種加鎖方式,先把上述案例的問題給解決了再說.
使用關鍵字synchronized,此處使用的是給普通方法加synchronized修飾的方法(除此之外,synchronized還可以修飾代碼塊和靜態方法)
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();//此時對象中的count值默認就是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
首先說明:這是有編譯器優化導致的,其次要知道cpu讀取變量時:先從主內存將變量的值存至緩存或者寄存器中,cpu計算時再在寄存器中讀取這個值.
當某線程頻繁的從內存中讀取一個不變的變量時,編譯器將會把從內存獲取變量的值直接優化成從寄存器直接獲取.之所以這樣優化,是因為,cpu從主內存中讀取一個變量比在緩存或者寄存器中讀取一個變量的值慢成千上萬倍,如果每每在內存中讀到的都是同一個值,既然緩存里頭已經有這個值了,干嘛還大費周折再去主內存中進行獲取呢,直接從緩存中直接讀取就可以了,可提升效率.
但是:一旦一個線程被優化成上述的情況,那如果有另一個線程把內存中的值修改了,我被優化的線程還傻乎乎的手里拿著修改之前的值呢,或者內存中的變量值被修改了,被優化的線程此時已經感應不到了.
具體而言:
public class Demo3 { private static boolean flag=false; public static void main(String[] args) { Thread t1=new Thread(()->{ while(!flag){ System.out.println("我是優化完之后直接讀取寄存器中的變量值才打印的哦!"); } }); t1.start(); flag=true; System.out.println("我已經在主線程中修改了標志位"); } }
運行上述代碼之后,程序并不會終止,而是一直在那打印t1線程中的打印語句.
如何解決上述問題:
引入關鍵字volatile:防止內存可見性問題,修飾一個變量,那某線程想獲取該變量的值的時候,只能去主內存中獲取,其次它還可以防止指令重排序,指令重排問題會在線程安全的單例模式(懶漢)進行介紹.具體:
public class Demo3 { private static volatile boolean flag=false; public static void main(String[] args) { Thread t1=new Thread(()->{ while(!flag){ System.out.println("我是優化完之后直接讀取寄存器中的變量值才打印的哦!"); } }); t1.start(); try { Thread.sleep(1);//主線程給t1留有充足的時間先跑起來 } catch (InterruptedException e) { e.printStackTrace(); } flag=true; System.out.println("我已經在主線程中修改了標志位"); } } //打印若干t1中的打印語句之后,主線程main中修改標志位之后,可以終止t1
注意:上述優化現象只會出現在頻繁讀的情況,如果不是頻繁讀,就不會出現那樣的優化.
生活案例:買菜
如果是傻乎乎的按照菜單從上到下的去買菜,從路線圖可以看出,不必要的路是真的沒少走.
如果執行代碼時,編譯器認為某些個代碼調整一下順序并不會影響結果,那代碼的執行順序就會被調整,就比如可以把上面買菜的順序調整成:黃瓜->蘿卜->青菜->茄子
單線程這樣的指令重排一般不會出現問題,但是多線程并發時,還這樣優化,就容易出現問題
針對這樣的問題,如果是針對一個變量,我們可以使用volatile修飾,如果是針對代碼塊,我們可以使用synchronized.
synchronized起作用的本質
修飾普通方法
修飾靜態方法
修飾代碼塊
因為我們知道java中所有類都繼承了Object,所以所有類都包含了Object的部分,我們可以稱這繼承的部分是"對象頭",使用synchronized進行對象頭中的標志位的修改,就可以做到一個對象的鎖一個時刻只能被一個線程所持有,其他線程此時不可搶占.這樣的設置,就好像把一個對象給鎖住了一樣.
如前述兩個線程給同一個count進行自增的案例.不再贅述.此時的所對象就是Counter對象
與普通方法類似.只不過這個方法可以類名直接調用.
首先修飾代碼塊需要執行鎖對象是誰,所以這里可以分為三類,一個是修飾普通方法的方法體這個代碼塊的寫法,其次是修飾靜態方法方法體的寫法,最后可以單獨寫一個Object的對象,來對這個Object對象進行上鎖.
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){//注意這里鎖的是類對象哦 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中這種隨手拿一個對象就能上鎖的用法,是java中一種很有特色的用法,在別的語言中,都是有專門的鎖對象的.
java中的線程狀態,以及如何區分線程安全問題 罪惡之源是搶占式執行多線程對同一個變量進行修改,多線程只讀一個變量是沒有線程安全問題的修改操作是非原子性的內存可見性引起的線程安全問題指令重排序引起的線程安全問題 synchronized的本質和用法
1.java中的線程狀態,以及如何區分
2.線程安全問題
罪惡之源是搶占式執行
多線程對同一個變量進行修改,多線程只讀一個變量是沒有線程安全問題的
修改操作是非原子性的
內存可見性引起的線程安全問題
指令重排序引起的線程安全問題
3.synchronized的本質和用法
關于“Java中線程狀態+線程安全問題+synchronized的用法是什么”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Java中線程狀態+線程安全問題+synchronized的用法是什么”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。