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 單例模式 總結整理