Basic Of Concurrency(四:線程安全)

共享資源可以被多個線程訪問且不會造成竟態條件即爲線程安全的代碼。因此分清哪些資源爲共享資源,對於區分代碼是否爲線程安全相當重要。html

線程安全與共享資源

局部變量

基礎數據類型

局部變量基礎數據類型僅會存儲在線程棧中,供本線程使用,因此局部變量基礎數據類型是線程安全的。java

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()。所以該成員變量是線程不安全的。bash

對以上實例稍加修改:多線程

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分別在不一樣的線程中建立新的實例,這樣就會是線程安全的。併發

事實證實,只要措施得當就能讓自己不安全的資源變成安全的。函數

線程控制逃逸規則

若是一個資源的建立使用和銷燬都在同一個線程內完成,且永遠不會脫離該線程的控制,則這個資源永遠是線程安全的。post

資源能夠是對象,數組,文件,數據庫,套接字等。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仍然是線程不安全的。使用不可變類型來規避線程安全問題須要牢記這點。

總結

線程安全與共享資源息息相關,因此肯定哪些資源是線程不安全的,對於區分代碼是不是線程安全的相當重要。

雖然不可變類型能夠規避線程安全問題,可是要當心不可變類型的引用,引用不是線程安全的,仍然會致使整個代碼是線程不安全的。

該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: 競態條件和臨界區
下一篇: Java內存模型

相關文章
相關標籤/搜索