1. 程式人生 > >從零開始學多執行緒之共享物件(二)

從零開始學多執行緒之共享物件(二)

想要使用多執行緒程式設計,有一個很重要的前提,那就是必須保證操縱的是執行緒安全的類.

那麼如何構建執行緒安全的類呢? 1. 使用同步來避免多個執行緒在同一時間訪問同一資料. 2. 正確的共享和安全的釋出物件,使多個執行緒能夠安全的訪問它們.

那麼如何正確的共享和安全的釋出物件呢? 這正是這篇部落格要告訴你的.

1. 多執行緒之間的可見性問題.

為什麼在多執行緒條件下需要正確的共享和安全的釋出物件呢?

這要說到可見性的問題:

 在多執行緒環境下,不能保證一個執行緒修改完共享物件的資料,對另一個執行緒是可見的.

一個執行緒讀到的資料也許是一個過期資料,這會導致嚴重且混亂的問題,比如意外的異常,髒的資料結構,錯誤的計算和無限的迴圈.

舉個例子:

public class NoVisibility {
    private static int num;
    private static boolean ready;

    private static class RenderThread extends Thread{
        @Override
        public void run(){
            while(!ready){
                Thread.yield();
            }
            System.out.println(
"num = " + num); } } public static void main(String [] args) throws InterruptedException { new RenderThread().start(); num = 42; ready = true; } }

new RenderThread().start()表示建立一個新執行緒,並執行執行緒內的run()方法 ,如果ready的值是false,執行Thread.yield()方法(當前執行緒休息一會讓其他執行緒執行),這時候再交給main方法的主執行緒執行,給num賦值42,ready賦值true,然後在任務執行緒中輸出num的值.因為可見性的問題,任務執行緒可能沒有看到主執行緒對num賦值,而輸出0.

我們接下來來看看釋出物件也會引發的可見性問題.

2. 什麼是釋出一個物件

釋出: 讓物件內被當前範圍之外的程式碼所使用.

public class Publish {
    public int num1;

    private int num2;

    public int getNum2(){
        return this.num2;
    }
}

無論是 publish.num1 還是  publish.getNum2()哪種方法,只要能在類以外的地方獲取到物件,我們就稱物件被髮布了.

如果一個物件在沒有完成構造的情況下就釋出了,這種情況叫逸出.逸出會導致其他執行緒看到過期值,危害執行緒安全.

常見的逸出的情況:

1.最常見的逸出就是將物件的引用放到公共靜態域(public static Object obj),釋出物件的引用,而在區域性方法中例項化這個物件.

public class Test {
    public static Set<Object> set;

    public void initialize(){
        set = new HashSet<>();
    }
}

2.釋出物件的狀態,而且狀態是可變的(沒用final修飾),或狀態裡包含其他的可變資料.

public class UnsafeStates {
    private String [] states = new String[]{"a","b","c"};

    public String[] getStates(){
        return states;
    }
}

3.在構造方法中使用內部類. 內部類的例項包含了對封裝實隱含的引用.

public class UnsafeStates {

    private Runnable r;

    public UnsafeStates() {
        r = new Runnable() {
            @Override
            public void run() {                 // 內部類在物件沒有構造好的情況下,已經可以this引用,逸出了
                // do something;
            }
        };
    }
}

逸出主要會導致兩個方面的問題: 

1. 釋出執行緒以外的任何執行緒都能看到物件的域的過期值,因而看到的是一個null引用或者舊值,即使此刻物件已經被賦予了新值.
2. 執行緒看到物件的引用是最新的,但是物件的狀態卻是過期的.我們已經瞭解了逸出的問題,那麼如何安全的釋出一個物件呢?

為了安全地釋出物件,物件的引用以及物件的狀態必須同時對其他執行緒可見(也就是說安全釋出就是保證物件的可見性),一個正確建立的物件可以通過下列條件安全釋出:

1. 通過靜態初始化器初始化物件的引用.
public class NoVisibility {

    public static Object obj = new Object();

}

2. 將它的引用儲存到volatile域或AtomicReference;
public class NoVisibility {

    public volatile Object obj = new Object();

}
Volatile可以保證可見性.效能消耗也只比非volatile多一點,但是不要過度依賴volatile變數,它比使用鎖的程式碼更脆弱,更難以理解,使用volatile的最佳方式就是用它來做退出迴圈的條件.使用volatile的例子:
public class Cycle {
    private  boolean condition;

    public  void loop(){
        while (condition){
            //do something..
        }
    }

    public void changeCondition(){
        if(condition == true){
            condition = false;
        }else{
            condition = true;
        }
    }
}
3. 將它的引用儲存到正確建立的物件的final域中.
public class NoVisibility {

    public final Object obj = new Object();

}
4. 或者將它的引用儲存到由鎖正確儲存的域中.
public class NoVisibility {

    private Hashtable<String,Object> hashtable = new Hashtable<>();

    public void  setHashtable(){
        Object obj = new Object();
        hashtable.put("obj",obj);
    }

}

不限於HashTable,只要是執行緒安全的容器都行

現在我們瞭解瞭如何安全的釋出一個物件,那麼問題來了,是否所有物件都需要安全釋出?安全釋出的物件是否就是執行緒全的了?

讓我們繼續往下看.

3.如何構建一個執行緒安全的類.我們先來回答上面的第一個疑問,是否所有物件都需要安全釋出?答案都是否定的.要回答這個問題,我們先簡單瞭解一下以下的三種物件:
1.不可變物件2.高效不可變物件3.可變物件1.不可變物件:建立後不能被修改的物件叫不可變物件,不可變物件天生是執行緒安全的.不可變物件不僅僅是所有域都是final型別的,只有滿足如下狀態才是不可變物件:
1.1 它的狀態不能在建立後改變.(包括狀態包含的其他值也不可做修改,比如狀態是一個集合list,list裡面的值也不可以修改,或者狀態是一個物件,那麼物件的狀態也不更改)
1.2.所有域都是final型別的.1.3.它被正確建立(建立期間沒有this引用的逸出)
  用高效不可變物件可以簡化開發,並由於減少了同步的使用,還會提高效能.3. 可變物件: 就是可變物件.

下面就是三種物件的釋出機制,釋出物件的必要條件依賴於物件的可變性:

1. 不可變物件可以通過任意機制釋出;
2. 高效不可變物件必須要安全地釋出;
3. 可變物件必須要安全釋出,同時必須要執行緒安全或者是被鎖保護.
最後一個問題安全釋出的物件是否就是執行緒全的了?
安全釋出只能保證物件釋出時的可見性,所以要保證執行緒的安全就要根據物件的可變性,通過同步+安全釋出來保證執行緒安全.關於同步和執行緒安全的知識可以看我的上一篇部落格
這兩篇部落格的知識點加在一起就可以構建執行緒安全類了.在下一篇部落格中,我會為大家介紹一些構建執行緒安全類的模式,這些模式讓類更容易成為執行緒安全的,並且不會讓程式意外破壞這些類的執行緒安全性.本期分享就到這了,我們下篇再見!