【併發編程】安全發佈對象—單例模式升級版

發佈對象

使一個對象能夠被當前範圍之外的代碼所使用,將創建的對象保存到容器中,也可能通過某個方法返回
對象的引用,或者將引用傳遞到其他類的方法中

對象逸出

一種錯誤的發佈,當一個對象還沒有構造完成時,就使它被其他線程所見

1、發佈的對象只需要被它需要的線程被看見
2、避免對象逸出

發佈錯誤對象:

import java.util.Arrays;
//線程不安全的
//發佈對象
public class Student {

    private String[] student = {"張三","李四","王五"};


    public String[] getStudent(){
        return student;
    }

    public static void main(String[] args) {
        Student unsarePublish = new Student();
        System.out.println(Arrays.toString(unsarePublish.getStudent()));

        unsarePublish.getStudent()[1]="趙柳";
        System.out.println(Arrays.toString(unsarePublish.getStudent()));
    }
}

返回結果:

[張三, 李四, 王五]
[張三, 趙柳, 王五]

上述例子中我們可以看到,李四已經被趙柳替代,通過public 類的訪問級別,發佈了這些域,在外部都可以訪問這些域,這樣的發佈對象其實是不安全的,因爲無法假設其他線程會不會修改這個域,所以會導致student的值是不確定的,因此是線程不安全的。

如何來進行安全的發佈對象呢:
看了網上好多博客,這裏也整理了一下,我們就來使用最經典的,單例模式來設計

1 懶漢模式設計

1.1 懶漢模式

/**
 * 懶漢模式
 * 線程不安全
 */
public class LazyMode1 {

    private LazyMode1(){//私有的構造函數}

    
    private static LazyMode1 instance = null;//單例對象


    //在單線程下是沒有問題的
    public static LazyMode1 getInstance(){
        if(instance == null){
            instance = new LazyMode1();
        }
        return  instance;
    }

}

這是一個比較正常的單例模式的,是一個線程不安全的類,那麼如何讓他變成一個線程安全的類呢,看下面一個例子

1.2 synchronized
/**
 * 懶漢模式
 * 線程安全
 */
public class LazyMode2 {

    private LazyMode2(){//私有的構造函數}

    private static LazyMode2 instance = null; //單例對象

    public  synchronized static LazyMode2 getInstance(){
        if(instance == null){
            instance = new LazyMode2();
        }
        return  instance;
    }

在這裏插入圖片描述

添加了synchronized後,在同一時間,只能允許一個線程訪問,因此可以保證這個是線程安全的
雖然他是線程安全的,但是他帶來的性能上的開銷,而這個開銷是我們不希望的,不推薦使用,別擔心下面還有更好的,我們來看下面的知識點

1.3 雙重同步鎖
/**
 * 懶漢模式
 * 但是這個類並不是線程安全的類
 */
public class LazyMode3 {


    private LazyMode3(){//私有的構造函數}

    private static LazyMode3 instance = null; //單例對象


    public static LazyMode3 getInstance(){
        if(instance == null){
            synchronized(LazyMode3.class){
                if(instance == null){
                    instance = new LazyMode3(); 
                }
            }
        }
        return  instance;
    }

}

這個案例,我們使用了雙重同步鎖的單例模式,但是他並不是一個線程安全的類,因爲在JVM和cpu優化,發生了指令重排,在單線程下,是沒有影響的,但是在多線程下,就會打亂分配的內存空間和初始化對象的順序,就會導致我們的結果和預期的不一致,雖然這個發生的概率很小,但是會發生,所以他是線程不安全的類,那麼如何能夠讓他成爲一個線程安全的類呢,看下面的例子。

1.4 volatile+雙重同步鎖
/**
 * 懶漢模式
 * 線程安全
 */
public class LazyMode4 {

    private LazyMode4(){//私有的構造函數}

    private volatile static LazyMode4 instance = null;

    public static LazyMode4 getInstance(){

        if(instance == null){
            synchronized(LazyMode4.class){
                if(instance == null){
                    instance = new LazyMode4();
                }
            }
        }
        return  instance;
    }

}

在這裏插入圖片描述

在這裏呢,我們使用了volatile+雙重檢測機制 他可以禁止指令重排

爲什麼vloatle可以禁止指令重排?
1、通過加入內存屏障和禁止重排序優化來實現
2、對volatile變量寫操作時,會在寫操作後加入一條store屏障指令,講本地內存中的共享變量值刷新到主內存中
3、對volatile變量讀操作時,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量
在這裏插入圖片描述
在這裏插入圖片描述

2 餓漢模式設計


/**
 * 餓漢模式
 * 線程安全的
 */
public class HungryMode1 {

    
    private HungryMode1(){//私有的構造函數}

    
    private static HungryMode1 instance = new HungryMode1();//單例對象

    public static HungryMode1 getInstance(){
        return instance;
    }

}


  • 餓漢模式的不足:如果餓漢模式中存在過多的處理,會導致這個類在加載的時候特別慢,可能會引起性能問題,如果是隻進行類的加載,沒有實際的調用的話,會導致資源的浪費
  • 所以我們需要考慮的一個點就是肯定會被使用,這樣纔不會導致過多的浪費,我們來看第二個例子
/**
 * 餓漢模式
 * 單例實例在類裝載時進行創建
 */
@ThreadSafe
public class HungryMode2 {

    
    private HungryMode2(){//私有的構造函數}


    //單例對象
    private static HungryMode2 instance = null;
	//靜態代碼塊
    static {
        instance = new HungryMode2();
    }



    public static HungryMode2 getInstance(){
        return instance;
    }


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

在這裏呢我們要注意的是:
當我們寫單例對象和靜態代碼塊的時候呢,一定要注意他們的順序,順序不一樣,執行結果會 有所不同

首先我們來看單例對象在靜態代碼塊前面

 //單例對象
    private static HungryMode2 instance = null;
	//靜態代碼塊
    static {
        instance = new HungryMode2();
    }

執行結果:

2061475679
2061475679

首先我們來看靜態代碼塊在單例對象前面

//靜態代碼塊
  static {
        instance = new HungryMode2();
    }
    //單例對象
    private static HungryMode2 instance = null;

執行結果:

Exception in thread "main" java.lang.NullPointerException
	at com.lyy.concurrency.singleton.HungryMode2.main(HungryMode2.java:37)

這裏會報空指針異常,這裏是爲什麼呢,是因爲如果靜態代碼塊在單例對象前面,會先執行靜態代碼塊,本來執行後已經有值了,但是到了下一步單例對象這裏,又會賦值爲空,所以就看看到我們執行的結果是空指針異常

我們可以debug看一下:
在這裏插入圖片描述

當我們執行的靜態代碼塊的時候是沒有值的
在這裏插入圖片描述

完成之後,我們發現instance是有值的,繼續往下走
在這裏插入圖片描述
到這裏後,我們會發現instance已經被設置成了Null

枚舉模式

/**
 * 枚舉模式
 */

public class EnumModel1 {

    //私有的構造函數
    private EnumModel1(){

    }

    public static EnumModel1 getInstance(){
            return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton{
        INSTANCE;

        private EnumModel1 singleton;

        //構造函數 
        Singleton(){
            singleton = new EnumModel1();
        }

        public EnumModel1 getInstance(){
            return singleton;
        }
    }

}

當我們通過枚舉來初始化這個對象的時候,JVM保證這個方法絕對只調用一次,並且是一個線程安全的類,不需要我們做過多的處理,使用起來也比較方便。所以我們推薦使用這種方式來進行安全發佈對象

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