Java併發編程的暗自努力(四)線程安全與共享資源

允許被多個線程同時執行的代碼稱作線程安全的代碼。線程安全的代碼不包含競態條件。當多個線程同時更新共享資源時會引發競態條件。因此,瞭解Java線程執行時共享了什麼資源很重要。

局部變量

  • 局部變量存儲在自己的棧中。也就是說,局部變量永遠也不會被多個線程共享。所以,基礎類型的局部變量是線程安全的。
public void someMethod(){
    long threadSafeInt = 0;
    threadSafeInt ++;
}

局部的對象引用

對象的局部引用與基礎類型的具備變量不同!
- 引用本身沒有被共享,但引用所指的對象沒有存儲在線程的棧內
如果在某個方法中創建的對象不會逃逸出(該對象不會被其它方法獲得,也不會被非局部變量引用到)該方法,那麼它是線程安全的

對象成員

對象成員存儲在堆上。如果兩個線程同時同時更新同一個對象的同一個成員,那麼這個代碼不是線程安全的。

有競態條件的示例:

NotThreadSafe sharedInstance = new NotThreadSafe();

new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();

public class MyRunnable implements Runnable{
  NotThreadSafe instance = null;

  public MyRunnable(NotThreadSafe instance){
    this.instance = instance;
  }

  public void run(){
    this.instance.add("some text");
  }
}

當然,如果傳入的對象不是同一個實例,那麼就不會導致競態條件

線程控制逃逸

  • 線程控制逃逸規則可以幫助你判斷代碼中對某些資源的訪問是否是線程安全的

如果一個資源的創建、使用,銷燬都在同一個線程內完成,且永遠不會脫離該線程的控制,則該資源的使用就是線程安全的

即使對象本身線程安全,但如果該對象中包含其他資源(文件,數據庫連接),整個應用也許就不再是線程安全的了。比如2個線程都創建了各自的數據庫連接,每個連接自身是線程安全的,但它們所連接到的同一個數據庫也許不是線程安全的。比如,2個線程執行如下代碼:

檢查記錄X是否存在,如果不存在,插入X

如果兩個線程同時執行,而且碰巧檢查的是同一個記錄,那麼兩個線程最終可能都插入了記錄:

線程1檢查記錄X是否存在。檢查結果:不存在
線程2檢查記錄X是否存在。檢查結果:不存在
線程1插入記錄X
線程2插入記錄X

同樣的問題也會發生在文件或其他共享資源上。因此,區分某個線程控制的對象是資源本身,還是僅僅到某個資源的引用很重要

線程安全及不可變性

當多個線程同時訪問同一個資源,並且其中的一個或者多個線程對這個資源進行了寫操作,纔會產生競態條件。多個線程同時讀同一個資源不會產生競態條件。

我們可以通過創建不可變的共享對象來保證對象在線程間共享時不會被修改,從而實現線程安全。如下示例:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }
}

請注意ImmutableValue類的成員變量value是通過構造函數賦值的,並且在類中沒有set方法。這意味着一旦ImmutableValue實例被創建,value變量就不能再被修改,這就是不可變性。但你可以通過getValue()方法讀取這個變量的值。

如果你需要對ImmutableValue類的實例進行操作,可以通過得到value變量後創建一個新的實例來實現,下面是一個對value變量進行加法操作的示例:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }

    public ImmutableValue add(int valueToAdd){
        return new ImmutableValue(this.value + valueToAdd);
    }
}

請注意add()方法以加法操作的結果作爲一個新的ImmutableValue類實例返回,而不是直接對它自己的value變量進行操作。

引用不是線程安全的!

重要的是要記住,即使一個對象是線程安全的不可變對象,指向這個對象的引用也可能不是線程安全的。看這個例子:

public void Calculator{
    private ImmutableValue currentValue = null;

    public ImmutableValue getValue(){
        return currentValue;
    }

    public void setValue(ImmutableValue newValue){
        this.currentValue = newValue;
    }

    public void add(int newValue){
        this.currentValue = this.currentValue.add(newValue);
    }
}

Calculator類持有一個指向ImmutableValue實例的引用。注意,通過setValue()方法和add()方法可能會改變這個引用。因此,即使Calculator類內部使用了一個不可變對象,但Calculator類本身還是可變的,因此Calculator類不是線程安全的。換句話說:ImmutableValue類是線程安全的,但使用它的類不是。當嘗試通過不可變性去獲得線程安全時,這點是需要牢記的。

要使Calculator類實現線程安全,將getValue()、setValue()和add()方法都聲明爲同步方法即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章