欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java中線程狀態(tài)+線程安全問題+synchronized的用法詳解

 更新時間:2022年04月11日 09:21:24   作者:一枚小比特  
這篇文章主要介紹了Java中線程狀態(tài)+線程安全問題+synchronized的用法詳解,本文結(jié)合示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下

java中的線程狀態(tài)??

在操作系統(tǒng)層面,一個線程就兩個狀態(tài):就緒和阻塞狀態(tài).

但是java中為了在線程阻塞時能夠更快速的知曉一個線程阻塞的原因,又將阻塞的狀態(tài)進行了細化.

  • NEW:線程對象已經(jīng)創(chuàng)建好了,但是系統(tǒng)層面的線程還沒創(chuàng)建好,或者說線程對象還沒調(diào)用start()
  • TERMINATED:系統(tǒng)中的線程已經(jīng)銷毀,但是代碼中的線程對象還在,也就是run()跑完了,Thread對象還在
  • RUNNABLE:線程位于就緒隊列,隨時都有可能被cpu調(diào)度執(zhí)行
  • TIMED_WAITING:線程執(zhí)行過程中,線程對象調(diào)用了sleep(),進入阻塞,休眠時間到了,就會回到就緒隊列
  • BLOCKED:有一個線程將一個對象上鎖(synchronized)之后,另一個線程也想給這個對象上鎖,就會陷入BLOCKED狀態(tài),只有第一個線程將鎖對象解鎖了,后一個線程才有可能給這個對象進行上鎖.
  • WAITING:搭配synchronized進行使用wait(),一旦一個線程調(diào)用了wait(),會先將所對象解鎖,等到另一個線程進行notify(),之后wait中的線程才會被喚醒,當然也可以在wait()中設(shè)置一個最長等待時間,防止出現(xiàn)死等.

線程安全問題案例分析??

多線程對同一變量進行寫操作??

  1. 概念:一串代碼什么時候叫作有線程安全問題呢?首先線程安全問題的罪惡之源是,多線程并發(fā)執(zhí)行的時候,會有搶占式執(zhí)行的現(xiàn)象,這里的搶占式執(zhí)行,執(zhí)行的是機器指令!那一串代碼什么時候叫作有線程安全問題呢?多線程并發(fā)時,不管若干個線程怎么去搶占式執(zhí)行他們的代碼,都不會影響最終結(jié)果,就叫作線程安全,但是由于搶占式執(zhí)行,出現(xiàn)了和預期不一樣的結(jié)果,就叫作有線程安全問題,出bug了!
  2. 典型案例:使用兩個線程對同一個數(shù)進行自增操作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

顯然預期結(jié)果是10w,但算出來就是6w多,這就是出現(xiàn)了線程安全問題.

分析原因:

僅針對每個線程的堆count進行自增的操作:首先要明白,進行一次自增的機器指令有三步:從主內(nèi)存中把count值拿到cpu寄存器中->把寄存器中的count值進行自增1->把寄存器中的count值刷新到主內(nèi)存中,我們姑且把這三步叫作:load->add->save

我們假設(shè)就是在一個cpu上(畫兩個cpu好表示)并發(fā)執(zhí)行兩組指令(就不會出現(xiàn)同時load這樣的情況了):

如出現(xiàn)上圖的情況:

觀察發(fā)現(xiàn):兩個線程都是執(zhí)行了一次count++,但是兩次++的結(jié)果卻不如意,相當于只進行了一次自增,上述就是出現(xiàn)了線程安全問題了.

并且我們可以預測出上述代碼的結(jié)果范圍:5w-10w之間!,為什么呢?

上面兩張圖表示的是出現(xiàn)線程安全問題的情況,表現(xiàn)的結(jié)果就是兩次加加當一次去用了,如果兩個線程一直處于這樣的狀態(tài)(也是最壞的狀態(tài)了),可不就是計算結(jié)果就是5w咯,那如果兩個線程一直是一個線程完整的執(zhí)行完load-add-save之后,另一個線程再去執(zhí)行這樣的操作,那就串行式執(zhí)行了,可不就是10w咯.

3.針對上述案例如何去解決呢?

案例最后也提到了,只要能夠?qū)崿F(xiàn)串行式執(zhí)行,就能保證結(jié)果的正確性,那java確實有這樣的功能供我們使用,即synchronized關(guān)鍵字的使用.

也就是說:cpu1執(zhí)行l(wèi)oad之前先給鎖對象進行加鎖,save之后再進行解鎖,cpu2此時才能去給那個對象進行上鎖,并進行一系列的操作.此時也就是保證了load-add-save的原子性,使得這三個步驟要么就別執(zhí)行,執(zhí)行就一口氣執(zhí)行完.

那你可能會提問,那這樣和只用一個main線程去計算自增10w次有什么區(qū)別,創(chuàng)建多線程還有什么意義呢?

意義很大,因為我們創(chuàng)建的線程很多時候不僅僅只是一個操作,光針對自增我們可以通過加鎖防止出現(xiàn)線程安全問題,但是各線程的其他操作要是不涉及線程安全問題那就可以并發(fā)了呀,那此時不就大大提升了執(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();//此時對象中的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

內(nèi)存可見性問題??

首先說明:這是有編譯器優(yōu)化導致的,其次要知道cpu讀取變量時:先從主內(nèi)存將變量的值存至緩存或者寄存器中,cpu計算時再在寄存器中讀取這個值.

當某線程頻繁的從內(nèi)存中讀取一個不變的變量時,編譯器將會把從內(nèi)存獲取變量的值直接優(yōu)化成從寄存器直接獲取.之所以這樣優(yōu)化,是因為,cpu從主內(nèi)存中讀取一個變量比在緩存或者寄存器中讀取一個變量的值慢成千上萬倍,如果每每在內(nèi)存中讀到的都是同一個值,既然緩存里頭已經(jīng)有這個值了,干嘛還大費周折再去主內(nèi)存中進行獲取呢,直接從緩存中直接讀取就可以了,可提升效率.

但是:一旦一個線程被優(yōu)化成上述的情況,那如果有另一個線程把內(nèi)存中的值修改了,我被優(yōu)化的線程還傻乎乎的手里拿著修改之前的值呢,或者內(nèi)存中的變量值被修改了,被優(yōu)化的線程此時已經(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)在主線程中修改了標志位");
    }
}

運行上述代碼之后,程序并不會終止,而是一直在那打印t1線程中的打印語句.

如何解決上述問題:

引入關(guān)鍵字volatile:防止內(nèi)存可見性問題,修飾一個變量,那某線程想獲取該變量的值的時候,只能去主內(nèi)存中獲取,其次它還可以防止指令重排序,指令重排問題會在線程安全的單例模式(懶漢)進行介紹.具體:

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留有充足的時間先跑起來
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=true;
        System.out.println("我已經(jīng)在主線程中修改了標志位");
    }
}
//打印若干t1中的打印語句之后,主線程main中修改標志位之后,可以終止t1

注意:上述優(yōu)化現(xiàn)象只會出現(xiàn)在頻繁讀的情況,如果不是頻繁讀,就不會出現(xiàn)那樣的優(yōu)化.

指令重排序問題??

生活案例:買菜

如果是傻乎乎的按照菜單從上到下的去買菜,從路線圖可以看出,不必要的路是真的沒少走.

如果執(zhí)行代碼時,編譯器認為某些個代碼調(diào)整一下順序并不會影響結(jié)果,那代碼的執(zhí)行順序就會被調(diào)整,就比如可以把上面買菜的順序調(diào)整成:黃瓜->蘿卜->青菜->茄子

單線程這樣的指令重排一般不會出現(xiàn)問題,但是多線程并發(fā)時,還這樣優(yōu)化,就容易出現(xiàn)問題

針對這樣的問題,如果是針對一個變量,我們可以使用volatile修飾,如果是針對代碼塊,我們可以使用synchronized.

synchronized的用法??

  • synchronized起作用的本質(zhì)
  • 修飾普通方法
  • 修飾靜態(tài)方法
  • 修飾代碼塊

synchronized起作用的本質(zhì)??

因為我們知道java中所有類都繼承了Object,所以所有類都包含了Object的部分,我們可以稱這繼承的部分是"對象頭",使用synchronized進行對象頭中的標志位的修改,就可以做到一個對象的鎖一個時刻只能被一個線程所持有,其他線程此時不可搶占.這樣的設(shè)置,就好像把一個對象給鎖住了一樣.

修飾普通方法??

如前述兩個線程給同一個count進行自增的案例.不再贅述.此時的所對象就是Counter對象

修飾靜態(tài)方法??

與普通方法類似.只不過這個方法可以類名直接調(diào)用.

修飾代碼塊??

首先修飾代碼塊需要執(zhí)行鎖對象是誰,所以這里可以分為三類,一個是修飾普通方法的方法體這個代碼塊的寫法,其次是修飾靜態(tài)方法方法體的寫法,最后可以單獨寫一個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中一種很有特色的用法,在別的語言中,都是有專門的鎖對象的.

Conclusion??

java中的線程狀態(tài),以及如何區(qū)分線程安全問題 罪惡之源是搶占式執(zhí)行多線程對同一個變量進行修改,多線程只讀一個變量是沒有線程安全問題的修改操作是非原子性的內(nèi)存可見性引起的線程安全問題指令重排序引起的線程安全問題 synchronized的本質(zhì)和用法

1.java中的線程狀態(tài),以及如何區(qū)分
2.線程安全問題

  • 罪惡之源是搶占式執(zhí)行
  • 多線程對同一個變量進行修改,多線程只讀一個變量是沒有線程安全問題的
  • 修改操作是非原子性的
  • 內(nèi)存可見性引起的線程安全問題
  • 指令重排序引起的線程安全問題

3.synchronized的本質(zhì)和用法

到此這篇關(guān)于Java中線程狀態(tài)+線程安全問題+synchronized的用法詳解的文章就介紹到這了,更多相關(guān)java線程安全synchronized用法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論