1. 程式人生 > >多執行緒Runnable匿名內部類一定要注意大坑

多執行緒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

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(...);

lower = value;

}

public void setUpper(int value) {

if (value < lower)

throw new IllegalArgumentException(...);

upper = value;

}

}

如果湊巧兩個執行緒在同一時間使用不一致的值執行 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修飾符。

綜上所述,以後想要模擬多個執行緒訪問共享變數的情況,千萬不要使用匿名內部類呀!不然就跳進一個大坑啦!