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