1. 程式人生 > >Java 單例模式 總結整理

Java 單例模式 總結整理

java 單例模式


分享總結常見的5種單例模式:


第一、單例模式的使用場景

A、Windows的任務管理器、回收站、文件系統如F盤,都是很典型的單例模式 ;

B、項目中,讀取配置文件的類,一般也是單例模式,沒有必要每次讀取都重新new一個對象加載

C、數據庫的連接池也是單例模式,因為數據庫鏈接是一種數據庫資源

D、在Spring中,每個bean默認都是單例的,可以很方便的交給Spring來管理

E、在servlet編程中,每個Servlet也是單例模式

還有像360瀏覽器每次打開都是新的客戶端,就不是單例模式;


第二、單例模式的核心是什麽?

一個類 只有 一個對象實例;

提供一個訪問接口。

// 其實,單例模式,說白了,就是控制一個對象的實例個數

//比方說,你可以實現 一個類只能存在5個實例個數,超過5個後,就不允許創建,這也是可以實現的。

第三、常見的5種單例模式?

1、餓漢式(線程安全,調用效率高,非延時加載)

2、懶漢式(線程安全,調用效率不高,延時加載)

3、雙重檢測鎖式(此種方式不建議使用)

4、靜態內部類式(線程安全,調用效率高,延時加載)

5、枚舉單例式(線程安全,調用效率高,非延時加載, 可以防止反射,反序列化漏洞)


第四、具體介紹

4.1 餓漢式

4.1.1 優點:

多線程安全

4.1.2 問題

非延遲加載。

如果加載後,以後都沒有使用的話,就白加載了,而且加載過程有可能很耗時。

4.1.3 代碼:

package com.xingej.patterns.singleton.example1;

/**
 * 單例模式:餓漢式
 * 
 * @author erjun 2017年11月7日 下午7:38:40
 */
public class HungryMethod {

    // static 變量會在類裝載時初始化,不會涉及到多線程問題。
    // 虛擬機保證只會裝載一次,肯定不會發生並發訪問的問題。
    // 因此,可以省略synchronized關鍵字的
    private static HungryMethod instance = new HungryMethod();

    private HungryMethod() {

    }

    // 問題:如果只是加載了本類,而沒有調用getInstance(),甚至永遠沒有調用,則會造成資源浪費
    public static HungryMethod getInstance() {
        return instance;
    }

}

4.2 懶漢式

4.2.1 優點:

多線程安全、延遲加載、資源利用率提高了

4.2.2 問題

並發效率低

由於:每次調用getInstance()方法,都會同步

4.2.3 代碼:

package com.xingej.patterns.singleton.example2;

/**
 * 懶漢式;延遲加載;
 * 
 * @author erjun 2017年11月7日 下午7:54:33
 */
public class LazyMethod {
    private static LazyMethod instance;

    private LazyMethod() {

    }

    // 方法同步,調用效率低
    public static synchronized LazyMethod getInstacne() {
        if (null == instance) {
            // 只有使用時,才調用
            instance = new LazyMethod();
        }

        return instance;
    }

}

4.3 雙重檢測鎖式

4.3.1 優點:

延遲加載、資源利用率提高了(第一次同步,以後不再同步)

4.3.2 問題

由於編譯器優化原因,JVM底層內部模型原因,偶爾會出問題,

因此,不建議使用

4.3.3 代碼:(註意:還有另外一種寫法,就是添加了兩個同步模塊,但是同樣不建議使用)

package com.xingej.patterns.singleton.example3;

/**
 * 雙重檢測實現單例模式 //-------不建議使用------此方法哦
 * 
 * @author erjun 2017年11月7日 下午8:08:43
 */
public class DoubleCheck {
    private static DoubleCheck instance;

    private DoubleCheck() {

    }

    // 提高了效率,只是在第一次需要同步;以後就不需要同步了
    // 但是
    // 由於編譯器優化原因和JVM底層內部模型元素,偶然會出問題。
    // -------不建議使用------
    public static DoubleCheck getInstance() {
        if (null == instance) {
            synchronized (DoubleCheck.class) {
                if (null == instance) {
                    return instance = new DoubleCheck();
                }
            }
        }

        return instance;
    }
}

4.4 靜態內部類

4.4.1 優點:

多線程安全、延遲加載

4.4.2 代碼:

package com.xingej.patterns.singleton.example4;

/**
 * 靜態內部類方式創建單例模式
 * 
 * 第一、當你初始化SingletonDemo01的時候,並不會立即加載靜態內部類的。 外部類沒有static屬性,則不會像餓漢式那樣立即加載對象。
 * 第二、只有真正調用getiInstance(),才會加載靜態內部類。 
 * 第三、加載類時是線程安全的, instance的類型是static final,
 * 從而保證了內存中只有這樣一個實例存在,而且只能被賦值一次, 從而保證了線程安全性 具有高並發和延遲加載的優點
 * 
 * @author erjun 2017年11月8日 上午11:38:31
 */
public class StaticInnerClass {
    // 靜態內部類,並不會立即加載的,只有調用時才加載的
    private static class SingletonClassInstance {
        private static final StaticInnerClass instance = new StaticInnerClass();
    }

    private StaticInnerClass() {

    }

    public static StaticInnerClass getInstance() {
        return SingletonClassInstance.instance;
    }

}

4.5 枚舉單例模式

4.5.1 優點:

實現簡單、線程安全

枚舉類本身就是單例模式,由JVM從根本上提供保障,避免通過反射和反序列化的漏洞!

4.5.2 缺點

無延遲加載

4.5.3 代碼

package com.xingej.patterns.singleton.example5;

/**
 * 枚舉的方式 實現 單例模式
 * 
 * 《優點》: 1、實現簡單;2、枚舉類本身就是單例模式,由JVM從根本上提供保障。避免通過反射和反序列化的漏洞進行創建實例對象 。《缺點》:無延遲加載
 * 
 * 
 * @author erjun 2017年11月8日 上午11:53:40
 */
public enum EnumMethod {
    INSTANCE;

    // 枚舉類可以有自己的行為模式的
    //下面這個方法,可以沒有的;
    public void show() {
        System.out.println("----我是枚舉類型的單例模式哦------");
    }

    // 測試用例
    public static void main(String[] args) {
        EnumMethod aDemo01 = EnumMethod.INSTANCE;
        EnumMethod bDemo01 = EnumMethod.INSTANCE;

        // 校驗是否是同一個對象
        System.out.println(aDemo01 == bDemo01);
    }
}


第五、選擇何種的單例模式呢?


場景一:占用資源少,不需要延時加載:

枚舉類 好於 餓漢式

場景二占用資源大,需要延時加載

靜態內部類 好於 懶漢式

第六、多線程方式測試5種單例模式的效率

package com.xingej.patterns.singleton;

import java.util.concurrent.CountDownLatch;

import org.junit.Test;

import com.xingej.patterns.singleton.example1.HungryMethod;
import com.xingej.patterns.singleton.example2.LazyMethod;
import com.xingej.patterns.singleton.example3.DoubleCheck;
import com.xingej.patterns.singleton.example4.StaticInnerClass;
import com.xingej.patterns.singleton.example5.EnumMethod;

/**
 * 多線程方式來測試5種單例模式的效率
 * 
 * @author erjun 2017年11月8日 下午4:07:14
 */
public class MulThread {
    private static final int threadNum = 10;

    private static final int count = 10 * 10000; // 調用單例 對象 10萬次

    private static CountDownLatch countDownLatch = new CountDownLatch(threadNum);

    // 餓漢式 效率測試
    @Test
    public void testEHanShi() throws Exception {

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < threadNum; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    for (int j = 0; j < count; j++) {
                        HungryMethod.getInstance();
                    }

                    countDownLatch.countDown();
                }
            }).start();
        }

        // 其實,所有的這些阻塞,內部其實都有一個死循環去監控的
        countDownLatch.await();

        long endTime = System.currentTimeMillis();
        System.out.println("----餓漢式---執行時間---:\t" + (endTime - startTime) + " 毫秒");

    }

    // 懶漢式 效率測試
    @Test
    public void testLanHanShi() throws Exception {

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < threadNum; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    for (int j = 0; j < count; j++) {
                        LazyMethod.getInstacne();
                    }

                    countDownLatch.countDown();
                }
            }).start();
        }

        // 其實,所有的這些阻塞,內部其實都有一個死循環去監控的
        countDownLatch.await();

        long endTime = System.currentTimeMillis();
        System.out.println("----懶漢式---執行時間---:\t" + (endTime - startTime) + " 毫秒");

    }

    // 雙重檢測 效率測試
    @Test
    public void test2Check() throws Exception {

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < threadNum; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    for (int j = 0; j < count; j++) {
                        DoubleCheck.getInstance();
                    }

                    countDownLatch.countDown();
                }
            }).start();
        }

        // 其實,所有的這些阻塞,內部其實都有一個死循環去監控的
        countDownLatch.await();

        long endTime = System.currentTimeMillis();
        System.out.println("----雙重檢測---執行時間---:\t" + (endTime - startTime) + " 毫秒");

    }

    // 靜態內部類 單例模式 效率測試
    @Test
    public void testStaticInnerClass() throws Exception {

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < threadNum; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    for (int j = 0; j < count; j++) {
                        StaticInnerClass.getInstance();
                    }

                    countDownLatch.countDown();
                }
            }).start();
        }

        // 其實,所有的這些阻塞,內部其實都有一個死循環去監控的
        countDownLatch.await();

        long endTime = System.currentTimeMillis();
        System.out.println("----靜態內部類方式---執行時間---:\t" + (endTime - startTime) + " 毫秒");

    }

    // 枚舉類方式的單例模式 效率測試
    @Test
    public void testEnumMethod() throws Exception {

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < threadNum; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    for (int j = 0; j < count; j++) {
                        // -------------------------------註意-------------------------------
                        // ----枚舉類,必須有接收值,就是等式左邊這些Object object, 不然有問題哦
                        Object object = EnumMethod.INSTANCE;
                    }

                    countDownLatch.countDown();
                }
            }).start();
        }

        // 其實,所有的這些阻塞,內部其實都有一個死循環去監控的
        countDownLatch.await();

        long endTime = System.currentTimeMillis();
        System.out.println("----枚舉類方式---執行時間---:\t" + (endTime - startTime) + " 毫秒");

    }

}

第七、實際案例:使用單例模式,讀取配置文件

代碼:

package com.xingej.patterns.singleton.example6;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class SingleMode {

    private SingleMode() {
        // 創建對象時,會默認加載配置文件
        readConfig();
    }

    private static class InnerSingle {
        private static SingleMode instance = new SingleMode();
    }

    public static SingleMode getInstance() {
        return InnerSingle.instance;
    }

    // ---------------------上面是單例模式----------------------------
    private Properties properties;

    private InputStream inputStream = null;

    private void readConfig() {
        properties = new Properties();

        try {
            // 註意,配置文件的路徑
            inputStream = SingleMode.class.getResourceAsStream("/single.properties");

            properties.load(inputStream);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != inputStream) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 提供對外的接口,
    // 查詢配置文件的屬性信息
    public String getProperties(String key) {
        return properties.getProperty(key);
    }
}

測試:

package com.xingej.patterns.singleton.example6;

import org.junit.Test;

public class SingleModeTest {

    // 通過單例方式,來讀取配置文件
    @Test
    public void testReadConfigBySingleMothod() {
        SingleMode instance = SingleMode.getInstance();

        String name = instance.getProperties("name");

        System.out.println("---name:\t" + name);
    }

}


總結:

設計模式一定拋開代碼,重點在思想

想想現實生活中,哪些是單例模式形式存在的



-------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------

下面這些內容,

可以不用關心,

一般情況下,不會用到的;


如果想擴展知識,可以看一下。

上面提到的5種單例模式中,除了枚舉單例模式外,其他4種單例模式都有bug的,

也就是說,可以通過

反射的方式

或者

反序列化的方式

創建兩個以上的對象,從而打破了單例模式只有一個對象的限定。


第一、反射的方式 破解單例模式的限定


添加測試用例,選擇懶漢式單例模式進行測試

技術分享從上面的結果中,看出來了,這個類,存在兩個對象了。


如何反破解呢?

技術分享


第二、反序列化的方式 破解單例模式的限定

使用反序列化方式的話,

前提條件時,單例模式必須實現Serializable接口,不然不能序列化的。

測試用例:

技術分享


如何反破解呢?

只需要在單例模式內,添加一個私有方法readResolve()即可。

具體代碼如下:

package com.xingej.patterns.singleton.example4;

import java.io.Serializable;

/**
 * 測試 通過 反序列化的方式,來破解單例模式
 * 
 * @author erjun 2017年11月8日 下午3:41:29
 */
public class StaticInnerClass2 implements Serializable {

    private static final long serialVersionUID = 1L;

    // 靜態內部類,並不會立即加載的,只有調用時才加載的
    private static class SingletonClassInstance {
        private static final StaticInnerClass2 instance = new StaticInnerClass2();
    }

    private StaticInnerClass2() {

    }

    public static StaticInnerClass2 getInstance() {
        return SingletonClassInstance.instance;
    }

    // 反序列化時,自動調用這個方法的
    // 直接返回對象,不需要再單獨創建新的對象了

    // 使用下面的方式,就可以
    // 阻止,通過序列化的方式來破解單例模式
    private Object readResolve() {
        return SingletonClassInstance.instance;
    }

}



代碼已上傳到git上:

https://github.com/xej520/xingej-design-patterns






















本文出自 “XEJ分布式工作室” 博客,請務必保留此出處http://xingej.blog.51cto.com/7912529/1980184

Java 單例模式 總結整理