Java併發(十七)----變量的線程安全分析

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 提供【安全】的意義所在。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章