1. 程式人生 > >單列模式與多執行緒

單列模式與多執行緒

 在23個標準設計模式中,單例模式在應用中還是很常見的,但是在多執行緒環境中,單例模式的使用有非常多的坑,使用好單例模式的一個原則:如何使單例模式在遇到多執行緒的環境中是安全的、正確的。下面分析幾種多執行緒的實現方式以及遇到的坑。

一、立即載入/餓漢模式

  立即載入:實用類的時候已經將物件建立完畢,常見的是直接new例項化,有“著急”,“急迫”的意思,因此也稱:“餓漢模式”。在呼叫方法前,已經例項化物件。程式碼如下:

單例模式:

複製程式碼

public class SingleTon01 {
    private  static SingleTon01 instance=new SingleTon01();

    public SingleTon01() {
        super();
    }
    public static SingleTon01 getInstance(){
        return instance;
    }
}

複製程式碼

執行緒:

複製程式碼

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(SingleTon01.getInstance().hashCode());
    }
}

複製程式碼

測試類:

複製程式碼

public class Run {
    public static void main(String[] args) {
        MyThread m1=new MyThread();
        MyThread m2=new MyThread();
        MyThread m3=new MyThread();
        MyThread m4=new MyThread();
        m1.start();
        m2.start();
        m3.start();
        m4.start();
    }
}

複製程式碼

執行結果:

  所有執行緒的物件hashCode均是一樣的,證明是單例模式,but,該程式碼的實現是優缺點的:不能有其他例項變數,因為getInstance方法沒有同步,可能會出現執行緒安全問題。

二、延遲載入/懶漢模式

  延遲載入:在呼叫方法的時候,物件才被例項化,常用的實現方式就是在方法內部例項化物件。程式碼如下:

單例模式:

複製程式碼

public class SingleTon02 {
    private  static SingleTon02 instance;

    public SingleTon02() {
        super();
    }
    public static SingleTon02 getInstance(){
        if(null==instance){
            instance=new SingleTon02();
        }
        return instance;
    }
}

複製程式碼

執行緒類:

複製程式碼

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(SingleTon02.getInstance().hashCode());
    }
}

複製程式碼

測試類:

複製程式碼

public class Run {
    public static void main(String[] args) {
        MyThread m1=new MyThread();
        MyThread m2=new MyThread();
        MyThread m3=new MyThread();
        MyThread m4=new MyThread();
        m1.start();
        m2.start();
        m3.start();
        m4.start();
    }
}

複製程式碼

執行結果:

  從執行結果來看,控制檯列印了多個hashCode值,說明該實現方式在多執行緒的環境中是失敗的,如何解決呢?其實很簡單,讓方法同步即可,使用synchronized關鍵字。改進後代碼吐下:

複製程式碼

public class SingleTon02 {
    private  static SingleTon02 instance;

    public SingleTon02() {
        super();
    }
    synchronized public static SingleTon02 getInstance(){
        try {
            if(null==instance){
                Thread.sleep(3000);//模擬業務處理
                instance=new SingleTon02();
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return instance;
    }
}

複製程式碼

再次執行:

  同步之後,證明該單例模式是正確的。但是,這種方式又帶來一種缺點,那就是效率問題,因為下一個執行緒必須需要等上一個執行緒釋放鎖之後才能執行,需要排隊執行,因此還可以優化,那就是:嘗試同步程式碼塊,針對重要程式碼進行單獨同步,以提升效率。

  下面總結了一種使用DCL雙檢查鎖機制實現單例模式,該模式適用於在多執行緒環境中的延遲載入單例模式設計。程式碼如下:

複製程式碼

public class SingleTon03 {
    private volatile static SingleTon03 instance;

    public SingleTon03() {
        super();
    }
    public static SingleTon03 getInstance(){
        try {
            if(null==instance){
                Thread.sleep(3000);//模擬業務處理
                synchronized (SingleTon03.class) {
                    if(null==instance){
                        instance=new SingleTon03();
                    }
                }
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return instance;
    }
}

複製程式碼

這種方式既保證了執行緒的安全性,還保證了效率。

 三、使用靜態內之類實現單例模式

  前面的改進方式可以實現在多執行緒的環境中實現單例模式,並且保證執行緒安全,那麼這種靜態內建類的方式也可以實現同樣的效果。建立靜態內類,如下:

複製程式碼

public class SingleTon04 {
    private static class SingleInner{
        private static SingleTon04 instance=new SingleTon04();
    }

    public SingleTon04() {
        super();
    }
    
    public static SingleTon04 getInstance(){
        return SingleInner.instance;
    }
}

複製程式碼

執行緒類:

複製程式碼

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(SingleTon04.getInstance().hashCode());
    }
}

複製程式碼

測試類同上

執行結果:

四、序列化與反序列化實現單例模式

  靜態內建類固然可以實現單例模式,但是這裡有一個坑,那就是在遇到序列化和反序列化的時候,依然會出現問題,依然會出現多個例項化物件,程式碼如下:

單例模式

複製程式碼

public class SingleTon05 implements Serializable{

    private static final long serialVersionUID = 888888L;
    //內部類方式
    private static class SingleTonInner{
        private static final SingleTon05 instance=new SingleTon05();
    }
    public SingleTon05() {
        super();
    }
    public static SingleTon05 getInstance(){
        return SingleTonInner.instance;
    }
    
}

複製程式碼

序列化執行類:

複製程式碼

public class Run2 {
    public static void main(String[] args) {
        //寫
        try {
            SingleTon05 singleTon05=SingleTon05.getInstance();
            FileOutputStream out=new FileOutputStream(new File("singleton05.txt"));
            ObjectOutputStream objectOutputStream=new ObjectOutputStream(out);
            
            objectOutputStream.writeObject(singleTon05);
            objectOutputStream.close();
            out.close();
            //列印hashcode
            System.out.println(singleTon05.hashCode());
        } catch (IOException e) {
            e.printStackTrace();
        }
        //讀
        try {
            FileInputStream in=new FileInputStream(new File("singleton05.txt"));
            ObjectInputStream objectInputStream=new ObjectInputStream(in);
            
            SingleTon05 singleTon05=(SingleTon05)objectInputStream.readObject();
            objectInputStream.close();
            in.close();
            //列印hashcode
            System.out.println(singleTon05.hashCode());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

複製程式碼

執行結果:

  很明顯,寫入和讀出來的物件不是一個,顯然不符合單例模式的設計模式。序列化破壞了單例模式,當然,還有一種破壞單例模式的方式,那就是反射,單例模式中儘量不要使用反射。呢麼問題來了,如何改進呢,其實很簡單,在序列化的時候在呼叫一個方法。改進如下:

複製程式碼

public class SingleTon05 implements Serializable{

    private static final long serialVersionUID = 888888L;
    //內部類方式
    private static class SingleTonInner{
        private static final SingleTon05 instance=new SingleTon05();
    }
    public SingleTon05() {
        super();
    }
    public static SingleTon05 getInstance(){
        return SingleTonInner.instance;
    }
    protected Object readResolve()throws ObjectStreamException {
        System.out.println("呼叫了readResolve方法!");
        return SingleTonInner.instance;
    }
}

複製程式碼

再次執行:

  序列化操作提供了一個很特別的鉤子(hook)-類中具有一個私有的被例項化的方法readresolve(),這個方法可以確保類的開發人員在序列化將會返回怎樣的object上具有發言權。這樣就確保我們在反序列化的時候返回的物件是同一個。

五、使用靜態程式碼塊實現單例模式

  靜態程式碼塊中的程式碼執行實在實用類的時候載入,因此我們可以應用靜態程式碼塊的這種特性來設計單例模式。程式碼如下:

複製程式碼

public class SingleTon06{

    private static  SingleTon06 instance=null;
    public SingleTon06() {
        super();
    }
    static{
        instance=new SingleTon06();
    }
    public static SingleTon06 getInstance(){
        return instance;
    }
}

複製程式碼

執行緒類測試類同三,結果如下:

六、使用列舉實現單例模式

   因為列舉和靜態程式碼塊的特性有相似之處,因此也可以使用這種特性來設計單例模式,這種模式非常簡單,也推薦時使用。程式碼如下:

複製程式碼

public enum SingleTon07{
    INSTANCE;

    private SingleTon07() {
    }
    
    public static SingleTon07 getInstance(){
        return INSTANCE;
    }
    
}

複製程式碼

測試執行類同上,結果如下:

  特點就是實現非常簡單。

瀋陽最好的胃腸醫院

煙臺哪個醫院做胃鏡檢查好

瀋陽做胃鏡多少錢

鄭州婦科醫院哪家好