5招教你實現多執行緒場景下的執行緒安全!

華為雲開發者社群發表於2021-08-11
摘要:多執行緒(併發)場景下,如何編寫執行緒安全(Thread-Safety)的程式,對於程式的正確和穩定執行有重要的意義。下面將結合示例,談談如何在Java語言中,實現執行緒安全的程式。

本文分享自華為雲社群《Java如何實現多執行緒場景下的執行緒安全》,作者: jackwangcumt 。

1 引言

當前隨著計算機硬體的快速發展,個人電腦上的CPU也是多核的,現在普遍的CUP核數都是4核或者8核的。因此,在編寫程式時,需要為了提高效率,充分發揮硬體的能力,則需要編寫並行的程式。Java語言作為網際網路應用的主要語言,廣泛應用於企業應用程式的開發中,它也是支援多執行緒(Multithreading)的,但多執行緒雖好,卻對程式的編寫有較高的要求。

單執行緒可以正確執行的程式不代表在多執行緒場景下能夠正確執行,這裡的正確性往往不容易被發現,它會在併發數達到一定量的時候才可能出現。這也是在測試環節不容易重現的原因。因此,多執行緒(併發)場景下,如何編寫執行緒安全(Thread-Safety)的程式,對於程式的正確和穩定執行有重要的意義。下面將結合示例,談談如何在Java語言中,實現執行緒安全的程式。

為了給出感性的認識,下面給出一個執行緒不安全的示例,具體如下:

package com.example.learn;
public class Counter {
    private static int counter = 0;
    public static int getCount(){
        return counter;
    }
    public static  void add(){
        counter = counter + 1;
    }
}

這個類有一個靜態的屬性counter,用於計數。其中可以通過靜態方法add()對counter進行加1操作,也可以通過getCount()方法獲取到當前的計數counter值。如果是單執行緒情況下,這個程式是沒有問題的,比如迴圈10次,那麼最後獲取的計數counter值為10。但多執行緒情況下,那麼這個結果就不一定能夠正確獲取,可能等於10,也可能小於10,比如9。下面給出一個多執行緒測試的示例:

package com.example.learn;
public class MyThread extends Thread{
    private String name ;
    public MyThread(String name){
        this.name = name ;
    }
    public void run(){
        Counter.add();
        System.out.println("Thead["+this.name+"] Count is "+  Counter.getCount());
    }
}
///////////////////////////////////////////////////////////
package com.example.learn;
public class Test01 {
    public static void main(String[] args) {
        for(int i=0;i<5000;i++){
            MyThread mt1 = new MyThread("TCount"+i);
            mt1.start();
        }
    }
}

這裡為了重現計數的問題,執行緒數調至比較大,這裡是5000。執行此示例,則輸出可能結果如下:

Thead[TCount5] Count is 4
Thead[TCount2] Count is 9
Thead[TCount4] Count is 4
Thead[TCount14] Count is 10
..................................
Thead[TCount4911] Count is 4997
Thead[TCount4835] Count is 4998
Thead[TCount4962] Count is 4999

注意:多執行緒場景下,執行緒不安全的程式輸出結果具有不確定性。

2 synchronized方法

基於上述的示例,讓其變成執行緒安全的程式,最直接的就是在對應的方法上新增synchronized關鍵字,讓其成為同步的方法。它可以修飾一個類,一個方法和一個程式碼塊。對上述計數程式進行修改,程式碼如下:

package com.example.learn;
public class Counter {
    private static int counter = 0;
    public static int getCount(){
        return counter;
    }
    public static synchronized void add(){
        counter = counter + 1;
    }
}

再次執行程式,則輸出結果如下:

......
Thead[TCount1953] Count is 4998
Thead[TCount3087] Count is 4999
Thead[TCount2425] Count is 5000

3 加鎖機制

另外一種常見的同步方法就是加鎖,比如Java中有一種重入鎖ReentrantLock,它是一種遞迴無阻塞的同步機制,相對於synchronized來說,它可以提供更加強大和靈活的鎖機制,同時可以減少死鎖發生的概率。示例程式碼如下:

package com.example.learn;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
    private  static int counter = 0;
    private static final ReentrantLock lock = new ReentrantLock(true);
    public static int getCount(){
        return counter;
    }
    public static  void add(){
        lock.lock();
        try {
            counter = counter + 1;
        } finally {
            lock.unlock();
        }
    }
}

再次執行程式,則輸出結果如下:

......
Thead[TCount1953] Count is 4998
Thead[TCount3087] Count is 4999
Thead[TCount2425] Count is 5000

注意:Java中還提供了讀寫鎖ReentrantReadWriteLock,這樣可以進行讀寫分離,效率更高。

4 使用Atomic物件

由於鎖機制會影響一定的效能,而有些場景下,可以通過無鎖方式進行實現。Java內建了Atomic相關原子操作類,比如AtomicInteger, AtomicLong, AtomicBoolean和AtomicReference,可以根據不同的場景進行選擇。下面給出示例程式碼:

package com.example.learn;
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
    private static final AtomicInteger counter = new AtomicInteger();
    public static int getCount(){
        return counter.get();
    }
    public static void add(){
        counter.incrementAndGet();
    }
}

再次執行程式,則輸出結果如下:

......
Thead[TCount1953] Count is 4998
Thead[TCount3087] Count is 4999
Thead[TCount2425] Count is 5000

5 無狀態物件

前面提到,執行緒不安全的一個原因就是多個執行緒同時訪問某個物件中的資料,資料存在共享的情況,因此,如果將資料變成獨享的,即無狀態(stateless)的話,那麼自然就是執行緒安全的。而所謂的無狀態的方法,就是給同樣的輸入,就能返回一致的結果。下面給出示例程式碼:

package com.example.learn;
public class Counter {
    public static int sum (int n) {
        int ret = 0;
        for (int i = 1; i <= n; i++) {
            ret += i;
        }
        return ret;
    }
}

6 不可變物件

前面提到,如果需要在多執行緒中共享一個資料,而這個資料給定值,就不能改變,那麼也是執行緒安全的,相當於只讀的屬性。在Java中可以通過final關鍵字進行屬性修飾。下面給出示例程式碼:

package com.example.learn;
public class Counter {
    public final int count ;
    public Counter (int n) {
        count = n;
    }
}

7 總結

前面提到了幾種執行緒安全的方法,總體的思想要不就是通過鎖機制實現同步,要不就是防止資料共享,防止在多個執行緒中對資料進行讀寫操作。另外,有些文章中說到,可以在變數前使用volatile修飾,來實現同步機制,但這個經過測試是不一定的,有些場景下,volatile依舊不能保證執行緒安全。雖然上述是執行緒安全的經驗總結,但是還是需要通過嚴格的測試進行驗證,實踐是檢驗真理的唯一標準。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章