1. 程式人生 > >java高併發(七)釋出物件

java高併發(七)釋出物件

釋出物件

釋出物件:是指使一個物件能夠被當前範圍之外的程式碼所使用。

物件逸出:一種錯誤的釋出。當一個物件還沒有構造完成時,就使它被其他執行緒所見。

我們經常需要釋出一些物件例如通過類的非私有方法返回物件的引用,或者通過共有靜態變數釋出物件。

簡單例子:

@Slf4j
@NotThreadSafe
public class UnsafePublish {
    private String[] states = {"a", "b", "c"};
    public String[] getStates() {
        return states;
    }

    public static void main(String[] args) {
        UnsafePublish  unsafePublish = new UnsafePublish();
        log.info("{}", Arrays.toString(unsafePublish.getStates()));
        unsafePublish.getStates()[0] = "d";
        log.info("{}", Arrays.toString(unsafePublish.getStates()));
    }
}

輸出:

15:42:12.937 [main] INFO com.vincent.example.publish.UnsafePublish - [a, b, c]
15:42:12.942 [main] INFO com.vincent.example.publish.UnsafePublish - [d, b, c]

為什麼要做這個例子?UnsafePublish通過getState()方法釋出了一個屬性,在類的任何外部執行緒都可以訪問這個屬性,這樣釋出物件是不安全的,因為我們無法確定其他執行緒會不會修改這個屬性,從而其他執行緒訪問這個屬性時,它的值是不確定的,這樣釋出的物件就是執行緒不安全的。

物件逸出

先看一個例子:

@Slf4j
@NotThreadSafe
@NotRecommend
public class Escape {
    private int thisCannBeEscape = 0;
    public Escape() {
        new InnerClass();
    }
    private class InnerClass {
        public InnerClass() {
            log.info("{}",Escape.this.thisCannBeEscape);
        }
    }

    public static void main(String[] args) {
        new Escape();
    }
}

這個例子中定義了一個內部類InnerClass,這個內部類的中引用了外部類的一個引用,有可能在物件沒有正確被構造之前就被髮布,有可能有不安全的因素在裡面。

安全的釋出物件

  • 在靜態初始化函式中初始化一個物件引用
  • 將物件的引用儲存到volatile型別域或者AtomicReference物件中
  • 將物件的引用儲存到某個正確構造物件的final型別域中
  • 將物件的引用儲存到一個由鎖保護的域中

懶漢模式 

/**
 * 懶漢模式
 * 單例的例項在第一次使用時進行建立
 */
public class SingletonExample1 {
    //私有建構函式
    private SingletonExample1(){

    }
    //單例物件
    private static SingletonExample1 instance = null;
    //靜態工廠方法
    public static SingletonExample1 getInstance(){
        if(instance == null){
            instance = new SingletonExample1();
        }
        return instance;
    }
}

 上面這段程式碼在單執行緒的情況下沒有問題,在多執行緒的情況下會有問題,問題出現在程式碼if(instance ==null){instance = new SingletonExample1();}return instance;部分。兩個執行緒可以同時訪問這段程式碼,因此這段程式碼有可能會被呼叫兩次,這樣兩個執行緒拿到的例項可能是不一樣的。這樣寫是執行緒不安全的。

懶漢模式-執行緒安全

@ThreadSafe
@NotRecommend
public class SingletonExample3 {
    //私有建構函式
    private SingletonExample3(){

    }
    //單例物件
    private static SingletonExample3 instance = null;
    //靜態工廠方法
    public static synchronized SingletonExample3 getInstance(){
        if(instance == null){
            instance = new SingletonExample3();
        }
        return instance;
    }
}

這樣的懶漢模式是執行緒安全,但是卻帶來了效能上的開銷。而這個開銷是我們不希望的。因此並不推薦這種寫法。

懶漢模式-雙重同步鎖單例模式


/**
 * 懶漢模式 -> 雙重同步鎖單例模式
 * 單例的例項在第一次使用時進行建立
 */
@NotThreadSafe
@ThreadSafe
@NotRecommend
public class SingletonExample4 {
    //私有建構函式
    private SingletonExample4(){

    }
    //單例物件
    private static SingletonExample4 instance = null;

    //正常的執行步驟
    //1.memory = allcate() 分配物件的記憶體空間
    //2.ctorInstance()初始化物件
    //3.instance = memory 設定instance指向剛分配的記憶體

    // JVM 和CPU優化,發生了指令重排
    //1.memory = allcate() 分配物件的記憶體空間
    //3.instance = memory 設定instance指向剛分配的記憶體
    //2.ctorInstance()初始化物件

    //靜態工廠方法
    public static SingletonExample4 getInstance(){
        if(instance == null){ //雙重檢測機制
            synchronized (SingletonExample4.class) { //同步鎖
                if (instance == null) {
                    instance = new SingletonExample4();
                }
            }
        }
        return instance;
    }
}
正常的執行步驟
1.memory = allcate() 分配物件的記憶體空間
2.ctorInstance()初始化物件
3.instance = memory 設定instance指向剛分配的記憶體
 上面的步驟在單執行緒的情況下沒有問題,而在多執行緒情況下JVM 和CPU優化,發生了指令重排
1.memory = allcate() 分配物件的記憶體空間
3.instance = memory 設定instance指向剛分配的記憶體
2.ctorInstance()初始化物件

發生指令重排導致雙重檢測機制執行緒不安全。因此可以限制不讓CPU發生指令重排。可以使用volatile關鍵字限制指令重排。

private static volatile SingletonExample5 instance = null;

這樣就可以實現執行緒安全了。

因此volatile關鍵字有兩個使用場景。1.狀態標示量。2.雙重檢測。這裡就是一個雙重檢測的應用。

餓漢模式

/**
 * 餓漢模式
 * 單例的例項在類裝載時進行建立
 */
@ThreadSafe
public class SingletonExample2 {
    //私有建構函式
    private SingletonExample2(){

    }
    //單例物件
    private static SingletonExample2 instance = new SingletonExample2();
    //靜態工廠方法
    public static SingletonExample2 getInstance(){
        return instance;
    }
}

如果單例類的建構函式中,沒有過多的操作處理,餓漢模式還可以接受。餓漢模式有什麼不足呢?如果建構函式中存在過多的處理,會導致這個類在載入的過程中特別慢,因此可能存在效能問題,如果使用餓漢模式的話只進行類的載入卻沒有實際的呼叫的話,會造成資源的浪費。因此使用餓漢模式時一定要考慮兩個問題,1.私有建構函式中沒有太多的處理。2.這個類肯定會被使用,不會帶來資源的浪費。餓漢模式屬於執行緒安全的。

餓漢模式2

@ThreadSafe
public class SingletonExample6 {
    //私有建構函式
    private SingletonExample6(){

    }
    static {
        instance = new SingletonExample6();
    }
    //單例物件
    private static SingletonExample6 instance = null;
    //靜態工廠方法
    public static SingletonExample6 getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(getInstance());
        System.out.println(getInstance());
    }
}

這時打印出來的值為null

需要把private static SingletonExample6 instance = null;程式碼寫在static語句塊之前:

/**
 * 餓漢模式
 * 單例的例項在類裝載時進行建立
 */
@ThreadSafe
public class SingletonExample6 {
    //私有建構函式
    private SingletonExample6(){

    }
    //單例物件
    private static SingletonExample6 instance = null;
    static {
        instance = new SingletonExample6();
    }

    //靜態工廠方法
    public static SingletonExample6 getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(getInstance());
        System.out.println(getInstance());
    }
}

當我們在寫靜態屬性和靜態程式碼塊時要注意順序。

列舉模式

/**
 * 餓漢模式
 * 單例的例項在類裝載時進行建立
 */
@ThreadSafe
@Recommend
public class SingletonExample7 {
    //私有建構函式
    private SingletonExample7(){

    }
    public static SingletonExample7 getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    private enum Singleton {
        INSTANCE;
        private SingletonExample7 singleton;

        // JVM保證這個構造方法絕對只調用一次
        Singleton() {
            singleton = new SingletonExample7();
        }
        public SingletonExample7 getInstance() {
            return singleton;
        }

    }

}

列舉模式相比於懶漢模式在安全性方面更容易保證,其次相比於餓漢模式可以在實際呼叫時才初始化,而在後續使用時也可以取到裡面的值。不會