Java多線程核心技術(五)單例模式與多線程

本文只須要考慮一件事:如何使單例模式遇到多線程是安全的、正確的html

1.當即加載 / "餓漢模式"

什麼是當即加載?當即加載就是使用類的時候已經將對象建立完畢,常見的實現辦法就是直接 new 實例化。編程

public class MyObject {
    private static MyObject myObject = new MyObject();

    public MyObject(){

    }

    public static MyObject getInstance(){
        return myObject;
    }


    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

打印結果:設計模式

985396398
985396398
985396398

控制檯打印的 hashCode 是同一個值,說明對象是同一個,也就實現了當即加載型單例設計模式。安全

此版本的缺點是不能有其餘其餘實例變量,由於getInstance()方法沒有同步,因此有可能出現非線程安全問題。多線程

2.延遲加載 / "懶漢模式"

什麼是延遲加載?延遲加載就是在調用 get() 方法時實例才被建立,常見的實現方法就是在 get() 方法中進行 new() 實例化。併發

測試代碼:ide

public class MyObject {
    private static MyObject myObject;

    public MyObject() {

    }

    public static MyObject getInstance() {
        try {
            if (myObject == null) {
                //模擬對象在建立以前作的一些準備工做
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }


    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

打印結果:測試

985396398
610025186
21895028

從運行結果來看,建立了三個對象,並非真正的單例模式。緣由顯而易見,3個線程同時進入了if (myObject == null) 判斷語句中,最後各自都建立了對象。.net

3.延遲加載解決方案

3.1 聲明synchronized關鍵字

既然多個線程能夠同時進入getInstance() 方法,那麼只須要對其進行同步synchronized處理便可。線程

public class MyObject {
    private static MyObject myObject;

    public MyObject() {

    }

    synchronized public static MyObject getInstance() {
        try {
            if (myObject == null) {
                //模擬對象在建立以前作的一些準備工做
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }


    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

打印結果:

961745937
961745937
961745937

雖然運行結果代表,成功實現了單例,但這種給整個方法上鎖的解決方法效率過低。

3.2 嘗試同步 synchronized 代碼塊

同步方法是對方法的總體加鎖,這對運行效率來說很不利的。改爲同步代碼塊後:

public class MyObject {
    private static MyObject myObject;

    public MyObject() {

    }

    public static MyObject getInstance() {
        try {
            synchronized (MyObject.class) {
                if (myObject == null) {
                    //模擬對象在建立以前作的一些準備工做
                    Thread.sleep(3000);
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }


    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

打印結果:

355159803
355159803
355159803

運行結果雖然代表是正確的,但同步synchronized語句塊依舊把整個 getInstance()方法代碼包括在內,和synchronize 同步方法效率是同樣低下。

3.3 針對某些重要的代碼進行單獨同步

因此,咱們能夠針對某些重要的代碼進行單獨的同步,而其餘的代碼則不須要同步。這樣在運行時,效率徹底能夠獲得大幅度提高。

public class MyObject {
    private static MyObject myObject;

    public MyObject() {

    }

    public static MyObject getInstance() {
        try {
            if (myObject == null) {
                //模擬對象在建立以前作的一些準備工做
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }


    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

運行結果:

985396398
21895028
610025186

此方法只對實例化對象的關鍵代碼進行同步,從語句的結構上來講,運行的效率的確獲得的提高。可是在多線程的狀況下依舊沒法解決獲得一個單例對象的結果。

3.4 使用DCL雙檢查鎖機制

在最後的步驟中,使用DCL雙檢查鎖機制來實現多線程環境中的延遲加載單例設計模式。

public class MyObject {
    private volatile static MyObject myObject;

    public MyObject() {

    }

    public static MyObject getInstance() {
        try {
            if (myObject == null) {
                //模擬對象在建立以前作的一些準備工做
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    if (myObject == null) {
                        myObject = new MyObject();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }


    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

運行結果:

860826410
860826410
860826410

使用DCL雙重檢查鎖功能,成功地解決了「懶漢模式」遇到多線程的問題。DCL也是大多數多線程結合單例模式使用的解決方案。

volatile 在此處的做用是:

1)可見性:保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。

2)有序性:禁止進行指令重排序。當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,在其後面的操做確定尚未進行。

擴展閱讀:關於volatile解決DCL(雙重檢查)問題的見解

4.使用靜態內置類實現單例模式

DCL能夠解決多線程單例模式的非線程安全問題。固然,還有許多其它的方法也能達到一樣的效果。

public class MyObject {
    public static class  MyObjectHandle{
        private static MyObject myObject = new MyObject();
        public static MyObject getInstance() {
            return myObject;
        }
    }

    public static MyObject getInstance(){
        return MyObjectHandle.getInstance();
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }
}

打印結果:

1035057739
1035057739
1035057739

靜態內置類能夠達到線程安全問題,但若是遇到序列化對象時,使用默認的方式運行獲得的結果仍是多例的。

解決方法就是在反序列化中使用readResolve()方法:

public class MyObject implements Serializable {
    //靜態內部類
    public static class  MyObjectHandle{
        private static final MyObject myObject = new MyObject();
    }

    public static MyObject getInstance(){
        return MyObjectHandle.myObject;
    }

    protected Object readResolve(){
        System.out.println("調用了readResolve方法");
        return MyObjectHandle.myObject;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        MyObject myObject = MyObject.getInstance();
        FileOutputStream outputStream = new FileOutputStream(new File("myObject.txt"));
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(myObject);
        objectOutputStream.close();
        System.out.println(myObject.hashCode());
        
        FileInputStream inputStream =  new FileInputStream(new File("myObject.txt"));
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        MyObject object = (MyObject) objectInputStream.readObject();
        objectInputStream.close();
        System.out.println(object.hashCode());
    }
}

運行結果:

621009875
調用了readResolve方法
621009875

5.使用static代碼塊實現單例模式

靜態代碼塊中的代碼在使用類的時候就已經執行了,因此能夠應用靜態代碼塊的這個特色來實現單例設計模式。

public class MyObject {
    private static MyObject myObject = null;
    static {
        myObject = new MyObject();
    }
    public static MyObject getInstance(){
        return myObject;
    }
    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

運行結果:

355159803
355159803
355159803

6.使用enum枚舉數據類型實現單例模式

枚舉enum 和靜態代碼塊的特性類似,在使用枚舉類時,構造方法會被自動調用,也能夠應用其這個特性實現單例設計模式。

public enum Singleton {
    INSTANCE;
    private MyObject myObject = null;
    Singleton() {
        myObject = new MyObject();
    }
    public MyObject getInstance(){
        return myObject;
    }

    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.INSTANCE.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.INSTANCE.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.INSTANCE.getInstance().hashCode());
            }
        }).start();
    }
}

運行結果:

1516133987
1516133987
1516133987

這樣實現的一個弊端就是違反了「職責單一原則」,完善後的代碼以下:

public class MyObject {
    public enum Singleton {
        INSTANCE;
        private MyObject myObject = null;

        Singleton() {
            myObject = new MyObject();
        }

        public MyObject getInstance() {
            return myObject;
        }
    }

    public static MyObject getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

運行結果:

610025186
610025186
610025186

7.文末總結

本文使用若干案例來闡述單例模式與多線程結合遇到的狀況與解決方案。

參看

《Java多線程編程核心技術》高洪巖著

擴展

Java多線程編程核心技術(一)Java多線程技能

Java多線程編程核心技術(二)對象及變量的併發訪問

Java多線程編程核心技術(三)多線程通訊

Java多線程核心技術(四)Lock的使用

Java多線程核心技術(六)線程組與線程異常

相關文章
相關標籤/搜索