溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

線程的安全性是什么

發(fā)布時間:2021-10-12 14:57:21 來源:億速云 閱讀:146 作者:iii 欄目:編程語言

本篇內(nèi)容介紹了“線程的安全性是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!

第一步:無狀態(tài)類

這里我們寫一個簡單的線程安全類,簡單到什么地步呢?如下所示

public class SafeDemo {

    public int sum(int n, int m){
        return n + m;
    }
}

就是這么簡單,我們說這個類是線程安全的

為啥安全呢?

因為這個類沒有狀態(tài),即無狀態(tài)類;

只有局部變量n,m,而這些局部變量是存在于棧中的,棧是每個線程獨有的,不跟其他線程共享,堆才共享

所以每個線程操作sum時,對應(yīng)的n,m只有自己可見,當(dāng)然就安全了

好了,通過上面的例子,我們知道了什么是線程安全類,那本節(jié)的內(nèi)容就到此結(jié)束了,再見

線程的安全性是什么

上面的例子,我們舉了一個無狀態(tài)類,接下來我們添加一個狀態(tài)試試

第二步:加一個狀態(tài)變量

加一個狀態(tài)變量(靜態(tài)屬性),代碼如下

public class UnSafeDemo {

    static int a = 0;

    public static void main(String[] args) throws InterruptedException {
				// 線程1
        new Thread(()-> {
            for(int j=0;j<100000;j++){
                a++;
            }
          
        }).start();
				// 線程2
        new Thread(()-> {
            for(int j=0;j<100000;j++){
                a++;
            }
        }).start();
				
        Thread.sleep(3000);
      	// 這里不是每次運行都會輸出200,000
        System.out.println(a);
    }
}

上面我們創(chuàng)建了兩個線程,每個線程都執(zhí)行10萬次的自增操作

但是因為自增不是原子操作,實際分三步:讀-改-寫

此時如果兩個線程同時讀到相同的值,則累加次數(shù)就會少一次

這種在并發(fā)編程中,由于不恰當(dāng)?shù)膱?zhí)行時序而出現(xiàn)不正確的結(jié)果的情況,叫做競態(tài)條件

如下圖所示:

期望的是正常執(zhí)行,每個線程交替執(zhí)行

線程的安全性是什么

結(jié)果卻有可能是不正常的,如下

線程的安全性是什么

這時我們就可以說,上面加的這個狀態(tài)是不安全的,結(jié)果就是整個類也是不安全的

不安全的狀態(tài)有二:

  • 可變狀態(tài)(變量):非final修飾的變量

  • 共享狀態(tài)(變量):非局部變量

像上面這個例子,狀態(tài)就同時屬于可變狀態(tài)和共享狀態(tài)

那要怎么確保安全:

  1. 同步:synchronized、volatile、顯式鎖、原子變量(比如AtomicInteger)

  2. 不可變變量:final(都不能改了,當(dāng)然安全了)

  3. 不共享變量:不在多線程中共享變量(即局部變量)

PS:代碼的封裝性越好,訪問可變變量的代碼塊越少,越容易確保線程安全

這里的自增我們就可以用同步中的原子變量來解決

關(guān)于原子變量的細節(jié),后面章節(jié)再介紹,這里只需要知道,原子變量內(nèi)部的操作是原子操作就可以了

修改后的代碼如下:

public class SafeDemo {
    static final AtomicInteger a = new AtomicInteger(0);
//    static int a = 0;

    public static void main(String[] args) throws InterruptedException {
				// 線程1
        new Thread(()-> {
            for(int j=0;j<100000;j++){
              	// 這里的自增是原子操作
                a.incrementAndGet();
            }
        }).start();
				// 線程2
        new Thread(()-> {
            for(int j=0;j<100000;j++){
              // 這里的自增是原子操作
                a.incrementAndGet();
            }
        }).start();

        Thread.sleep(3000);
        System.out.println(a.get());
    }
}

可以看到,加了AtomicInteger.incrementAndGet()方法,這個方法是原子操作

這時,不管怎么運行,都是輸出200,000

第三步:加多個狀態(tài)變量

上面我們加了一個狀態(tài)變量,可以用原子變量來保證線程安全

那如果是多個狀態(tài)變量呢?此時就算用了原子變量也不行了

因為原子變量只是保證它內(nèi)部是原子操作,但是當(dāng)多個原子變量放到一起組合操作時,他們之間又存在競態(tài)條件了,就又不是原子操作了

競態(tài)條件:并發(fā)編程中,由于不恰當(dāng)?shù)膱?zhí)行時序而出現(xiàn)不正確的結(jié)果的情況,就是競態(tài)條件(重復(fù)陳述ing,加深記憶)

代碼如下:

public class UnSafeDemo2 {
    static final AtomicInteger a = new AtomicInteger(0);
    static final AtomicInteger b = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {

        new Thread(()-> {
            for(int j=0;j<10000;j++){
                a.incrementAndGet();
                b.incrementAndGet();
                if(a.get()!=b.get()){
                    // 理想狀態(tài)的話,不會運行到這里,因為a和b是一起自增的
                    // 但是大部分時候都是不正常的,因為a和b各自是原子操作,但是放到一起就不是原子操作了
                    System.out.println(1);
                }
            }
        }).start();

        new Thread(()-> {
            for(int j=0;j<10000;j++){
                a.incrementAndGet();
                b.incrementAndGet();
                if(a.get()!=b.get()){
                    // 理想狀態(tài)的話,不會運行到這里,因為a和b是一起自增的
                    // 但是大部分時候都是不正常的,因為a和b各自是原子操作,但是放到一起就不是原子操作了
                    System.out.println(2);
                }
            }
        }).start();
    }
}

上面多次運行,會發(fā)現(xiàn)基本上每次都會打印1和2,就是因為這兩個線程之間存在競態(tài)條件

那怎么解決呢?

上鎖

代碼如下:

public class UnSafeDemo2 {
    static final AtomicInteger a = new AtomicInteger(0);
    static final AtomicInteger b = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        // 單獨創(chuàng)建一個對象,用來充當(dāng)鎖
        UnSafeDemo2 unSafeDemo2 = new UnSafeDemo2();
        new Thread(()-> {
            for(int j=0;j<10000;j++){
                // 這里加了鎖
                synchronized (unSafeDemo2){
                    a.incrementAndGet();
                    b.incrementAndGet();
                    if(a.get()!=b.get()){
                        // 現(xiàn)在肯定是理想狀態(tài),不會運行到這里
                        System.out.println(1);
                    }
                }
            }
        }).start();

        new Thread(()-> {
            for(int j=0;j<10000;j++){
                // 這里加了鎖
                synchronized (unSafeDemo2){
                    a.incrementAndGet();
                    b.incrementAndGet();
                    if(a.get()!=b.get()){
                        // 現(xiàn)在肯定是理想狀態(tài),不會運行到這里
                        System.out.println(2);
                    }
                }
            }
        }).start();
    }
}

這里用到的鎖為內(nèi)置鎖,還有很多其他鎖,這里就不展開了(后面章節(jié)再介紹)

這里要注意:同步代碼必須上同一個鎖才有用,比如上面的例子,兩個線程都是上的unsafeDemo2這個鎖

官人們可以試一下,一個上unsafeDemo2鎖,一個上Object鎖,看會輸出啥

內(nèi)置鎖也叫監(jiān)視器鎖

特點:

  • 互斥性:即一個線程持有鎖,其他線程就要等待鎖釋放后才可以獲取鎖

  • 可重入性:如果某個線程嘗試去獲取一個鎖,而這個鎖之前就是這個線程所持有的,那么這個線程就可以再次獲取到鎖

    • 縮小鎖的范圍

    • 將耗時長的操作(前提是操作與狀態(tài)無關(guān)),放到同步之外的代碼塊

    • 跟狀態(tài)有關(guān)的方法都需要上鎖:操作麻煩,其實就是類的每個方法都需要上鎖,如果后面添加了一個方法,忘記加鎖,那還是有安全問題(比如被官人們遺棄的Vector)

    • 性能問題:整個方法都上鎖,性能很低,尤其是一些耗時操作,比如網(wǎng)絡(luò)IO這種容易阻塞的操作

    • 避免了死鎖:比如一個子類繼承父類的synchronized方法,并顯示調(diào)用父類的synchronized方法,如果不可重入,那么在子類中獲取的鎖,調(diào)用子類的fun方法是沒問題的,但是調(diào)用父類的fun方法時,會提示上了鎖,從而被阻塞,此時就會死鎖(自己持有鎖,還有再去獲取鎖,但是又獲取不到)

    • 好處:

    • 缺點:

    • 解決:

好了,差不多先這些吧,后面還有太多東西了,慢慢來吧。

畢竟我們都一大把年紀(jì)了,身體要緊吶。

總結(jié)

懶了懶了,直接貼圖了(敲的腦仁疼),圖做的不是很好,不過應(yīng)該能看懂,望見諒哈

線程的安全性是什么

“線程的安全性是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI