Concurrency(四:執行緒安全)

MenfreXu發表於2019-03-11

共享資源能夠被多個執行緒訪問且不會形成竟態條件即為執行緒安全的程式碼。所以分清哪些資源為共享資源,對於區分程式碼是否為執行緒安全至關重要。

執行緒安全與共享資源

區域性變數

基礎資料型別

區域性變數基礎資料型別僅會儲存線上程棧中,供本執行緒使用,所以區域性變數基礎資料型別是執行緒安全的。

public void safeMethod() {
	int threadSafeInt = 10;
    System.out.println(++threadSafeInt);
}
複製程式碼
物件引用

雖然引用型別本身不會被共享,但引用型別指向的物件是儲存在共享堆中的,因此需要判斷堆中的物件是否會逃逸出本執行緒,被其他執行緒訪問到,若該物件僅會在本執行緒被訪問,那麼它是執行緒安全的,若它能夠被其他執行緒所訪問那麼它將不是執行緒安全的。

public void method0(){
    LocalObject localObject = new LocalObject();
    localObject.add(2);    
	method1(localObject);
}

public void method1(LocalObject localObject){
	localObject.add(5);
}
複製程式碼

method0即使被多個執行緒呼叫也不會產生競態條件,因為區域性物件僅會線上程內部建立和訪問,即使在method0將該區域性物件傳遞給本物件的其他方法或其他物件的方法也是如此。

物件成員

若多個執行緒訪問多一個物件的成員,將會產生竟態條件。

public class UnSafeObject {

    private int count = 0;

    public void add(int val) {
        int result = this.count + val;
        this.count = result;
    }

    public int getCount() {
        return this.count;
    }

    public static void main(String[] args) {
        final UnSafeObject unSafeObject = new UnSafeObject();
        Runnable runnable = () -> {
            unSafeObject.add(2);
        };
        IntStream.range(1, 3)
                .forEach(i -> new Thread(runnable).start());
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

該例項中,兩個執行緒併發訪問了UnSafeObject物件的成員方法add()。因此該成員變數是執行緒不安全的。

對以上例項稍加修改:

public static void main(String[] args) {
	Runnable runnable = () -> {
        final UnSafeObject unSafeObject = new UnSafeObject();
    	unSafeObject.add(2);
    };
	IntStream.range(1, 3)
    	.forEach(i -> new Thread(runnable).start());
	try {
    	Thread.sleep(2000L);
    } catch (InterruptedException e) {
    e.printStackTrace();
	}
}
複製程式碼

讓UnSafeObject分別在不同的執行緒中建立新的例項,這樣就會是執行緒安全的。

事實證明,只要措施得當就能讓本身不安全的資源變成安全的。

執行緒控制逃逸規則

如果一個資源的建立使用和銷燬都在同一個執行緒內完成,且永遠不會脫離該執行緒的控制,則這個資源永遠是執行緒安全的。

資源可以是物件,陣列,檔案,資料庫,套接字等。Java物件的銷燬可以指沒有任何引用指向該物件。

就算一個物件本身是執行緒安全的,但是該物件中包含其他不安全資源(檔案,資料庫連線),則整個物件都不再是執行緒安全的。

如多個執行緒建立的資料庫連線指向同一個資料庫,則有可能多個執行緒對同一條記錄作出更新或插入,此時該資料庫資源是執行緒不安全的。

執行軌跡如下:

執行緒1: 檢查記錄x是否存在,記錄x不存在
執行緒2: 檢查記錄x是否存在,記錄x不存在
執行緒1: 插入記錄x
執行緒2: 插入記錄x
複製程式碼

最後資料庫會產生兩條一樣的記錄,不符合預期。

執行緒安全與不可變性

之所以會產生競態條件是因為一到多個執行緒同時訪問了相同的物件,且至少有一個執行緒進行了寫操作。多個執行緒訪問不會變化的物件並不會產生執行緒安全問題。因此可以利用物件的不可變性來規避執行緒安全問題。不可變性僅能保證物件在多個執行緒間傳遞是安全的。但凡有執行緒對傳遞的物件進行操作,都會產生新的物件。

done like this:

public class ImmutableExample {
    private int val;

    public ImmutableExample(int val) {
        this.val = val;
    }

    public int getVal() {
        return this.val;
    }

    public ImmutableExample add(int val) {
        return new ImmutableExample(this.val + val);
    }
}
複製程式碼

不可變物件僅能通過建構函式注入數值,沒有更新入口,當需要更新資料時,僅能通過構造新物件來實現。

不可變型別的引用

public class Calculator {
    private ImmutableExample immutableExample;

    public Calculator(ImmutableExample immutableExample) {
        this.immutableExample = immutableExample;
    }

    public ImmutableExample getImmutableExample() {
        return immutableExample;
    }

    public void add(ImmutableExample immutableExample) {
        this.immutableExample = this.immutableExample.add(immutableExample.getVal());
    }
}
複製程式碼

例項中Calculator內部引用了一個不可變物件,雖然引用的物件是不可變的。但引用本身卻是可變的,因此在多執行緒環境下,整個Calculator仍然是執行緒不安全的。使用不可變型別來規避執行緒安全問題需要牢記這點。

總結

執行緒安全與共享資源息息相關,所以確定哪些資源是執行緒不安全的,對於區分程式碼是否是執行緒安全的至關重要。

雖然不可變型別可以規避執行緒安全問題,但是要小心不可變型別的引用,引用不是執行緒安全的,仍然會導致整個程式碼是執行緒不安全的。

相關文章