1、成員變數和靜態變數是否執行緒安全
-
如果它們沒有共享,則執行緒安全
-
如果它們被共享了,根據它們的狀態是否能夠改變,又分兩種情況
-
如果只有讀操作,則執行緒安全
-
如果有讀寫操作,則這段程式碼是臨界區,需要考慮執行緒安全
-
-
區域性變數是執行緒安全的
-
但區域性變數引用的物件則未必
-
如果該物件沒有逃離方法的作用訪問,它是執行緒安全的
-
如果該物件逃離方法的作用範圍,需要考慮執行緒安全
-
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 提供【安全】的意義所在。