Java並行(十七)----變數的執行緒安全分析

2023-12-11 06:01:01

1、成員變數和靜態變數是否執行緒安全

  • 如果它們沒有共用,則執行緒安全

  • 如果它們被共用了,根據它們的狀態是否能夠改變,又分兩種情況

    • 如果只有讀操作,則執行緒安全

    • 如果有讀寫操作,則這段程式碼是臨界區,需要考慮執行緒安全

 

2、區域性變數是否執行緒安全

  • 區域性變數是執行緒安全的

  • 但區域性變數參照的物件則未必

    • 如果該物件沒有逃離方法的作用存取,它是執行緒安全的

    • 如果該物件逃離方法的作用範圍,需要考慮執行緒安全

3、區域性變數執行緒安全分析

public static void test1() {
    int i = 10;
    i++;
}

每個執行緒呼叫 test1() 方法時區域性變數 i,會在每個執行緒的棧幀記憶體中被建立多份,因此不存在共用

public static void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=0
         0: bipush        10
         2: istore_0
         3: iinc          0, 1
         6: return
      LineNumberTable:
        line 10: 0
        line 11: 3
        line 12: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3       4     0     i   I

如圖

區域性變數的參照稍有不同,先看一個成員變數的例子

class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 臨界區, 會產生競態條件
            method2();
            method3();
            // } 臨界區
        }
    }
​
    private void method2() {
        list.add("1");  // 存取的同一個成員變數list
    }
​
    private void method3() {
        list.remove(0);// 存取的同一個成員變數list
    }
}

執行

static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
    ThreadUnsafe test = new ThreadUnsafe();
    for (int i = 0; i < THREAD_NUMBER; i++) {
        new Thread(() -> {
            test.method1(LOOP_NUMBER);
        }, "Thread" + i).start();
    }
}

多執行幾次就會發現,其中一種情況是,如果執行緒2 還未 add,執行緒1 remove 就會報錯:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    at java.util.ArrayList.rangeCheck(ArrayList.java:657)
    at java.util.ArrayList.remove(ArrayList.java:496)
    at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35)
    at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26)
    at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
    at java.lang.Thread.run(Thread.java:748)

分析:

  • 無論哪個執行緒中的 method2 參照的都是同一個物件中的 list 成員變數

  • method3 與 method2 分析相同

將 list 修改為區域性變數

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
​
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
​
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

那麼就不會有上述問題了

分析:

  • list 是區域性變數,每個執行緒呼叫時會建立其不同範例,沒有共用

  • 而 method2 的引數是從 method1 中傳遞過來的,與 method1 中參照同一個物件

  • method3 的引數分析與 method2 相同

方法存取修飾符帶來的思考?

如果把 method2 和 method3 的方法修改為 public 會不會帶來執行緒安全問題?

  • 情況1:有其它執行緒呼叫 method2 和 method3

  • 情況2:在 情況1 的基礎上,為 ThreadSafe 類新增子類,子類覆蓋 method2 或 method3 方法,即

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
​
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
​
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
​
class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

這樣的話就會存線上程安全的問題。因為method3新開了一個執行緒,造成多個執行緒存取同一個共用資源,就會存線上程安全的問題。

從這個例子就可以看出 private 或 final 提供【安全】的意義所在。