Java實現單例的5種方式


#Java實現單例的5種方式

1. 什么是單例模式

單例模式指的是在應用整個生命周期內只能存在一個實例。單例模式是一種被廣泛使用的設計模式。他有很多好處,能夠避免實例對象的重復創建,減少創建實例的系統開銷,節省內存。

2. 單例模式和靜態類的區別

首先理解一下什么是靜態類,靜態類就是一個類里面都是靜態方法和靜態field,構造器被private修飾,因此不能被實例化。Math類就是一個靜態類。

知道了什么是靜態類后,來說一下他們兩者之間的區別:

1)首先單例模式會提供給你一個全局唯一的對象,靜態類只是提供給你很多靜態方法,這些方法不用創建對象,通過類就可以直接調用;

2)如果是一個非常重的對象,單例模式可以懶加載,靜態類就無法做到;

那么時候時候應該用靜態類,什么時候應該用單例模式呢?首先如果你只是想使用一些工具方法,那么最好用靜態類,靜態類比單例類更快,因為靜態的綁定是在編譯期進行的。如果你要維護狀態信息,或者訪問資源時,應該選用單例模式。還可以這樣說,當你需要面向對象的能力時(比如繼承、多態)時,選用單例類,當你僅僅是提供一些方法時選用靜態類。

3.如何實現單例模式

1. 餓漢模式

所謂餓漢模式就是立即加載,一般情況下再調用getInstancef方法之前就已經產生了實例,也就是在類加載的時候已經產生了。這種模式的缺點很明顯,就是占用資源,當單例類很大的時候,其實我們是想使用的時候再產生實例。因此這種方式適合占用資源少,在初始化的時候就會被用到的類。

class SingletonHungary {
    private static SingletonHungary singletonHungary = new SingletonHungary();
    //將構造器設置為private禁止通過new進行實例化
    private SingletonHungary() {
        
    }
    public static SingletonHungary getInstance() {
        return singletonHungary;
    }
}

 

2. 懶漢模式

懶漢模式就是延遲加載,也叫懶加載。在程序需要用到的時候再創建實例,這樣保證了內存不會被浪費。針對懶漢模式,這里給出了5種實現方式,有些實現方式是線程不安全的,也就是說在多線程並發的環境下可能出現資源同步問題。

首先第一種方式,在單線程下沒問題,在多線程下就出現問題了。

// 單例模式的懶漢實現1--線程不安全
class SingletonLazy1 {
    private static SingletonLazy1 singletonLazy;

    private SingletonLazy1() {

    }

    public static SingletonLazy1 getInstance() {
        if (null == singletonLazy) {
            try {
                // 模擬在創建對象之前做一些准備工作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            singletonLazy = new SingletonLazy1();
        }
        return singletonLazy;
    }
}

 

我們模擬10個異步線程測試一下:

public class SingletonLazyTest {

    public static void main(String[] args) {

        Thread2[] ThreadArr = new Thread2[10];
        for (int i = 0; i < ThreadArr.length; i++) {
            ThreadArr[i] = new Thread2();
            ThreadArr[i].start();
        }
    }

}

// 測試線程
class Thread2 extends Thread {
    @Override
    public void run() {
        System.out.println(SingletonLazy1.getInstance().hashCode());
    }
}

 

 

運行結果:

124191239
124191239
872096466
1603289047
1698032342
1913667618
371739364
124191239
1723650563
367137303
 

 

可以看到他們的hashCode不都是一樣的,說明在多線程環境下,產生了多個對象,不符合單例模式的要求。

那么如何使線程安全呢?第二種方法,我們使用synchronized關鍵字對getInstance方法進行同步。

// 單例模式的懶漢實現2--線程安全
// 通過設置同步方法,效率太低,整個方法被加鎖
class SingletonLazy2 {
    private static SingletonLazy2 singletonLazy;

    private SingletonLazy2() {

    }

    public static synchronized SingletonLazy2 getInstance() {
        try {
            if (null == singletonLazy) {
                // 模擬在創建對象之前做一些准備工作
                Thread.sleep(1000);
                singletonLazy = new SingletonLazy2();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return singletonLazy;
    }
}

 

 

使用上面的測試類,測試結果:

1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
 

可以看到,這種方式達到了線程安全。但是缺點就是效率太低,是同步運行的,下個線程想要取得對象,就必須要等上一個線程釋放,才可以繼續執行。

那我們可以不對方法加鎖,而是將里面的代碼加鎖,也可以實現線程安全。但這種方式和同步方法一樣,也是同步運行的,效率也很低。

// 單例模式的懶漢實現3--線程安全
// 通過設置同步代碼塊,效率也太低,整個代碼塊被加鎖
class SingletonLazy3 {

    private static SingletonLazy3 singletonLazy;

    private SingletonLazy3() {

    }

    public static SingletonLazy3 getInstance() {
        try {
            synchronized (SingletonLazy3.class) {
                if (null == singletonLazy) {
                    // 模擬在創建對象之前做一些准備工作
                    Thread.sleep(1000);
                    singletonLazy = new SingletonLazy3();
                }
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
        }
        return singletonLazy;
    }
}

 

 

我們來繼續優化代碼,我們只給創建對象的代碼進行加鎖,但是這樣能保證線程安全么?

// 單例模式的懶漢實現4--線程不安全
// 通過設置同步代碼塊,只同步創建實例的代碼
// 但是還是有線程安全問題
class SingletonLazy4 {

    private static SingletonLazy4 singletonLazy;

    private SingletonLazy4() {

    }

    public static SingletonLazy4 getInstance() {
        try {
            if (null == singletonLazy) {        //代碼1
                // 模擬在創建對象之前做一些准備工作
                Thread.sleep(1000);
                synchronized (SingletonLazy4.class) {
                    singletonLazy = new SingletonLazy4(); //代碼2
                }
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
        }
        return singletonLazy;
    }
}

 

我們來看一下運行結果:

1210004989
1425839054
1723650563
389001266
1356914048
389001266
1560241484
278778395
124191239
367137303
 

從結果看來,這種方式不能保證線程安全,為什么呢?我們假設有兩個線程A和B同時走到了‘代碼1’,因為此時對象還是空的,所以都能進到方法里面,線程A首先搶到鎖,創建了對象。釋放鎖后線程B拿到了鎖也會走到‘代碼2’,也創建了一個對象,因此多線程環境下就不能保證單例了。

讓我們來繼續優化一下,既然上述方式存在問題,那我們在同步代碼塊里面再一次做一下null判斷不就行了,這種方式就是我們的DCL雙重檢查鎖機制。

//單例模式的懶漢實現5--線程安全
//通過設置同步代碼塊,使用DCL雙檢查鎖機制
//使用雙檢查鎖機制成功的解決了單例模式的懶漢實現的線程不安全問題和效率問題
//DCL 也是大多數多線程結合單例模式使用的解決方案
class SingletonLazy5 {

    private static SingletonLazy5 singletonLazy;

    private SingletonLazy5() {

    }

    public static SingletonLazy5 getInstance() {
        try {
            if (null == singletonLazy) {
                // 模擬在創建對象之前做一些准備工作
                Thread.sleep(1000);
                synchronized (SingletonLazy5.class) {
                    if(null == singletonLazy) {
                        singletonLazy = new SingletonLazy5();
                    }
                }
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
        }
        return singletonLazy;
    }
}

 

運行結果:

124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
 

我們可以看到DCL雙重檢查鎖機制很好的解決了懶加載單例模式的效率問題和線程安全問題。這也是我們最常用到的方式。

3. 靜態內部類

我們也可以使用靜態內部類實現單例模式,代碼如下:

//使用靜態內部類實現單例模式--線程安全
class SingletonStaticInner {
    private SingletonStaticInner() {
        
    }
    private static class SingletonInner {
        private static SingletonStaticInner singletonStaticInner = new SingletonStaticInner();
    }
    public static SingletonStaticInner getInstance() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return SingletonInner.singletonStaticInner;
    }
}

 

可以看到使用這種方式我們沒有顯式的進行任何同步操作,那他是如何保證線程安全呢?和餓漢模式一樣,是靠JVM保證類的靜態成員只能被加載一次的特點,這樣就從JVM層面保證了只會有一個實例對象。那么問題來了,這種方式和餓漢模式又有什么區別呢?不也是立即加載么?實則不然,加載一個類時,其內部類不會同時被加載。一個類被加載,當且僅當其某個靜態成員(靜態域、構造器、靜態方法等)被調用時發生。

可以說這種方式是實現單例模式的最優解。

4. 靜態代碼塊

這里提供了靜態代碼塊實現單例模式。這種方式和第一種類似,也是一種餓漢模式。

//使用靜態代碼塊實現單例模式
class SingletonStaticBlock {
    private static SingletonStaticBlock singletonStaticBlock;
    static {
        singletonStaticBlock = new SingletonStaticBlock();
    }
    public static SingletonStaticBlock getInstance() {
        return singletonStaticBlock;
    }
}

 

5. 序列化與反序列化

LZ為什么要提序列化和反序列化呢?因為單例模式雖然能保證線程安全,但在序列化和反序列化的情況下會出現生成多個對象的情況。運行下面的測試類,

public class SingletonStaticInnerSerializeTest {

    public static void main(String[] args) {
        try {
            SingletonStaticInnerSerialize serialize = SingletonStaticInnerSerialize.getInstance();
            System.out.println(serialize.hashCode());
            //序列化
            FileOutputStream fo = new FileOutputStream("tem");
            ObjectOutputStream oo = new ObjectOutputStream(fo);
            oo.writeObject(serialize);
            oo.close();
            fo.close();
            //反序列化
            FileInputStream fi = new FileInputStream("tem");
            ObjectInputStream oi = new ObjectInputStream(fi);
            SingletonStaticInnerSerialize serialize2 = (SingletonStaticInnerSerialize) oi.readObject();
            oi.close();
            fi.close();
            System.out.println(serialize2.hashCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

//使用匿名內部類實現單例模式,在遇見序列化和反序列化的場景,得到的不是同一個實例
//解決這個問題是在序列化的時候使用readResolve方法,即去掉注釋的部分
class SingletonStaticInnerSerialize implements Serializable {
    
    /**
     * 2018年03月28日
     */
    private static final long serialVersionUID = 1L;
    
    private static class InnerClass {
        private static SingletonStaticInnerSerialize singletonStaticInnerSerialize = new SingletonStaticInnerSerialize();
    }
    
    public static SingletonStaticInnerSerialize getInstance() {
        return InnerClass.singletonStaticInnerSerialize;
    }
    
//    protected Object readResolve() {
//        System.out.println("調用了readResolve方法");
//        return InnerClass.singletonStaticInnerSerialize;
//    }
}

 

可以看到:

865113938
1078694789
 

結果表明的確是兩個不同的對象實例,違背了單例模式,那么如何解決這個問題呢?解決辦法就是在反序列化中使用readResolve()方法,將上面的注釋代碼去掉,再次運行:

865113938
調用了readResolve方法
865113938
 

問題來了,readResolve()方法到底是何方神聖,其實當JVM從內存中反序列化地"組裝"一個新對象時,就會自動調用這個 readResolve方法來返回我們指定好的對象了, 單例規則也就得到了保證。readResolve()的出現允許程序員自行控制通過反序列化得到的對象。

 

————————————————
版權聲明:本文為CSDN博主「scuwangjun」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u014672511/article/details/79774847


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM