多執行緒Runnable匿名內部類一定要注意大坑
通常情況下,當需要模擬多執行緒的時候我們會選擇兩種方式。第一種就是自己實現Runnable類,然後在主類中呼叫我們自己實現的Runnable,例如:
package concurrent;
public class MyRunnable implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("自己實現的Runnable!");
}
}
package concurrent; public class Run { public static void main(String[] args) { MyRunnable myRun = new MyRunnable(); new Thread(myRun).start(); } }
但是為了測試方便,我們更喜歡的這種姿勢。凌厲幹練。反手就是一個匿名內部類。
package concurrent; public class Run { public static void main(String[] args) { //MyRunnable myRun = new MyRunnable(); //new Thread(myRun).start(); new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub System.out.println("自己實現的Runnable!"); } }).start();; } }
但是,這時候,就會有一個大坑在等著你調。
通常情況下,這兩種方式對測試是不會有什麼影響的。但是如果模擬的是多個執行緒搶佔資源,想要模擬多執行緒訪問共享變量出錯的問題,此時就該大大的注意了。還是舉個栗子比較好。
以下程式碼顯示了一個非執行緒安全的數值範圍類。它包含了一個不變式 —— 下界總是小於或等於上界。
清單 1. 非執行緒安全的數值範圍類
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
如果湊巧兩個執行緒在同一時間使用不一致的值執行 setLower
和 setUpper
的話,則會使範圍處於不一致的狀態。例如,如果初始狀態是 (0, 5)
,同一時間內,執行緒 A 呼叫 setLower(4)
並且執行緒 B 呼叫 setUpper(3)
,顯然這兩個操作交叉存入的值是不符合條件的,那麼兩個執行緒都會通過用於保護不變式的檢查,使得最後的範圍值是 (4, 3)
—— 一個無效值。
我們想要模擬出來結果(4,3)來驗證確實會出錯,如果按照上方提供的模擬多執行緒時候的兩種方式。
第一種方案,規規矩矩版。自己實現Runnable介面
業務類:
public class NumberRange {
private int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(value+" value > upper"+upper);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(value+" value < lower"+lower);
upper = value;
}
}
public class TestSub implements Runnable{
private NumberRange v;
public TestSub(NumberRange v) {
this.v = v;
}
@Override
public void run() {
// TODO Auto-generated method stub
v.setLower(4);
}
}
public class TestSup implements Runnable{
private NumberRange v;
public TestSup(NumberRange v) {
this.v = v;
}
@Override
public void run() {
// TODO Auto-generated method stub
v.setUpper(3);
}
}
兩個自己實現的Runnable執行緒,用來設定最大最小值。
public class VolatileLearn {
public static void main(String[] args) {
NumberRange num = new NumberRange();
num.setLower(0);
num.setUpper(5);
TestSub t = new TestSub(num);
TestSup t2 = new TestSup(num);
new Thread(t).start();
new Thread(t2).start();
}
}
最後是一個啟動測試類。此時進行多次的執行,會發現確實能夠出現(4,3)的錯誤情況。
我們再來看看第二種簡約乾淨的實現方案。(匿名內部類)
public class VolatileLearn {
public static void main(String[] args) {
NumberRange num = new NumberRange();
num.setLower(0);
num.setUpper(5);
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
num.setUpper(3);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
num.setLower(4);
}
}).start();
}
}
程式碼看起來確實清爽了很多,但是卻會發現再也模擬不出來錯誤的結果了。這是為什麼呢?
實際上在這種模擬多個執行緒訪問共享資源的時候是不能這樣乾的。因為匿名內部類裡邊訪問外部的變數,實際上都必須是final型別的變數,而final修飾的變數是執行緒安全的。因此也就模擬不出來出錯的結果了。
當然,這裡邊num變數沒有使用final修飾,是因為jdk8中,會自動在底層加上final修飾符。
綜上所述,以後想要模擬多個執行緒訪問共享變數的情況,千萬不要使用匿名內部類呀!不然就跳進一個大坑啦!