1. 程式人生 > >Java多執行緒核心技術(五)單例模式與多執行緒

Java多執行緒核心技術(五)單例模式與多執行緒

本文只需要考慮一件事:如何使單例模式遇到多執行緒是安全的、正確的

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() 例項化。

測試程式碼:

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) 判斷語句中,最後各自都建立了物件。

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也是大多數多執行緒結合單例模式使用的解決方案。

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多執行緒程式設計核心技術》高洪巖著

擴充套件

對於閱讀,我是看一遍,抄一遍~