轉:狂神說Java之徹底玩轉單例設計模式
徹底玩轉單例模式
參考文章:
單例模式:
簡介:
單例模式是一種常用的軟件設計模式,其定義是單例對象的類只能允許一個實例存在。
注意:
1、單例類只能有一個實例。
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。
適用場景:
1.需要生成唯一序列的環境 2.需要頻繁實例化然后銷毀的對象。 3.創建對象時耗時過多或者耗資源過多,但又經常用到的對象。 4.方便資源相互通信的環境
構建步驟:
- 將該類的構造方法定義為私有方法
- 這樣其他處的代碼就無法通過調用該類的構造方法來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例;
- 在該類內提供一個靜態實例化方法
- 當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,
- 如果類保持的引用為空就創建該類的實例並將實例的引用賦予該類保持的引用。
優缺點:
優點:
- 在內存中只有一個對象,節省內存空間;
- 避免頻繁的創建銷毀對象,可以提高性能;
- 避免對共享資源的多重占用,簡化訪問;
- 為整個系統提供一個全局訪問點。
缺點:
- 不適用於變化頻繁的對象;
- 濫用單例將帶來一些負面問題,如為了節省資源將數據庫連接池對象設計為的單例類,可能會導致共享連接池對象的程序過多而出現連接池溢出;
- 如果實例化的對象長時間不被利用,系統會認為該對象是垃圾而被回收,這可能會導致對象狀態的丟失;
餓漢式單例模式:
餓漢式:飢餓的人,看到食物就上來搶。對應在代碼中,在類加載時就立刻實例化。
public class HungrySingleton { //假如類中存在這樣的空間開辟的操作: //使用餓漢式時,不管用不用,上來就給你開了再說,造成空間浪費。 private byte[] data1 = new byte[1024*1024]; private byte[] data2 = new byte[1024*1024]; private byte[] data3 = new byte[1024*1024]; private byte[] data4 = new byte[1024*1024]; //1、私有構造器 private HungrySingleton(){ } //2、類的內部創建對象 private final static HungrySingleton HUNGRYSINGLE = new HungrySingleton(); //3、向外暴露一個靜態的公共方法。 getInstance public static HungrySingleton getInstance(){ return HUNGRYSINGLE; } public static void main(String[] args) { //單線程: HungrySingleton instance1 = HungrySingleton.getInstance(); HungrySingleton instance2 = HungrySingleton.getInstance(); System.out.println(instance1 == instance2);//true System.out.println("------------"); //多線程: new Thread(()->{ HungrySingleton instance_A = HungrySingleton.getInstance(); System.out.println(instance_A); //com.kuangstudy.Singleton.HungrySingleton@626213bf }).start(); new Thread(()->{ HungrySingleton instance_B = HungrySingleton.getInstance(); System.out.println(instance_B); //com.kuangstudy.Singleton.HungrySingleton@626213bf }).start(); } }
小結:
優點:
- 基於 classloader 機制避免了多線程的同步問題,在類裝載的時候就完成實例化。避免了線程同步問題==>線程安全。
- 沒有加鎖,執行效率會提高。
- 簡單好用。
缺點:
在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。
懶漢式單例模式:
非線程安全:
public class LazySingleton { //1.私有化構造函數 private LazySingleton() { System.out.println(Thread.currentThread().getName()+" ->OK"); } //2.創建對象(容器) private static LazySingleton lazyMan ; //3.對外提供靜態實例化方法,判斷在對象為空的時候創建 public static LazySingleton getInstance(){ //用的時候再加載 if (lazyMan == null) { lazyMan = new LazySingleton(); } return lazyMan; } //測試單線程下懶漢式單例 public static void main(String[] args) { LazySingleton instance1 = LazySingleton.getInstance(); LazySingleton instance2 = LazySingleton.getInstance(); //實例化兩個只出現:main ->OK System.out.println(instance1);//com.kuangstudy.Singleton.LazySingleton@61bbe9ba System.out.println(instance2);//com.kuangstudy.Singleton.LazySingleton@61bbe9ba } }
小結
這種方式是最基本的實現方式,這種實現最大的問題就是不支持多線程。因為沒有加鎖 synchronized,所以嚴格意義上它並不算單例模式。
這種方式 lazy loading 很明顯,不要求線程安全,在多線程不能正常工作。
public class LazySingleton { //1.私有化構造函數 private LazySingleton() { System.out.println(Thread.currentThread().getName()+" ->OK"); } //2.創建對象(容器) private static LazySingleton lazyMan ; //3.對外提供靜態實例化方法,判斷在對象為空的時候創建 public static LazySingleton getInstance(){ //用的時候再加載 if (lazyMan == null) { lazyMan = new LazySingleton(); } return lazyMan; } //開啟多條線程實例化LazySingleton public static void main(String[] args) { for (int i = 0; i < 100; i++) { // new Thread(()->{LazySingleton.getInstance();}).start(); new Thread(LazySingleton::getInstance).start(); } } //結果實例化超過1個對象:(不唯一) /* Thread-0 ->OK Thread-3 ->OK Thread-2 ->OK Thread-1 ->OK */ }
線程安全:
public class LazySingletonWithDCL { //1.私有化構造函數 private LazySingletonWithDCL() { System.out.println(Thread.currentThread().getName() + " ->OK"); } //2.創建對象(容器) // private static LazySingletonWithDCL lazyMan ; //5.new 不是一個原子性操作: private volatile static LazySingletonWithDCL lazyMan; //3.對外提供靜態實例化方法 //4.為保證線程安全,需要上鎖 public static LazySingletonWithDCL getInstance() { if (lazyMan == null) { synchronized (LazySingletonWithDCL.class) { if (lazyMan == null) { lazyMan = new LazySingletonWithDCL(); } } } return lazyMan; } //開啟多條線程實例化LazySingletonWithDCL public static void main(String[] args) { for (int i = 0; i < 100; i++) { // new Thread(()->{LazySingletonWithDCL.getInstance();}).start(); new Thread(LazySingletonWithDCL::getInstance).start(); } } }
問題:
為什么要synchronized (LazySingletonWithDCL.class)而不是對方法加鎖?
相關博客:Synchronized方法鎖、對象鎖、類鎖區別 (精)
簡單回答:
synchronized 重量級鎖,鎖的范圍越小越好,class只有一個,而方法會每次都執行,因此為了提高效率,鎖對象而不是鎖方法。
new對象的過程為什么不是一個原子性操作?
相關博客:new一個對象竟然不是原子操作?
實錘 new 對象不是原子性操作,會執行以下操作:
- 分配內存空間
- 執行構造方法,
- 初始化對象把對象指向空間實踐出真知:
這是原子類AtomicInteger.getAndIncrement()方法,反編譯后:
public class TestNew { private static int num = 0; public static void main(String[] args) { TestNew testNew = new TestNew(); } public void add(){ num++; }; }
反編譯后:

小結:
- 雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking)
- 這種方式采用雙鎖機制,安全且在多線程情況下能保持高性能。getInstance() 的性能對應用程序很關鍵。
- 這樣,實例化代碼只用執行一次,后面再次訪問時,判斷 if(lazyMan == null),直接return實例化對象,也避免的反復進行方法同步.
- 線程安全;延遲加載;效率較高
反射破壞單例:
在Java進階中學習過非常暴力的獲取類的方式:反射(詳見:注解與反射)
因此對於在單例模式中私有的方法,我們可以通過反射進行破解:
對DCL餓漢式單例進行破解:
實力詮釋再多的🔒也抵不住反射的暴力破解
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { LazySingletonWithDCL instance = LazySingletonWithDCL.getInstance(); //利用反射創建對象 //獲取無參構造 Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null); //開放權限 declaredConstructor.setAccessible(true); LazySingletonWithDCL instance_withReflect = declaredConstructor.newInstance(); System.out.println(instance); //LazySingletonWithDCL@61bbe9ba System.out.println(instance_withReflect); //LazySingletonWithDCL@610455d6 System.out.println(instance == instance_withReflect); //false }
單例模式增加校驗反擊:
因為反射走的是無參構造,可以在構造函數中進行判斷
public class LazySingletonWithDCL { //1.私有化構造函數 //6.增加對反射的判斷 private LazySingletonWithDCL() { synchronized (LazySingletonWithDCL.class){ //6.1如果此時已經有實例,阻止反射創建 if (lazyMan != null){ throw new RuntimeException("不要試圖通過反射破解單例"); } } System.out.println(Thread.currentThread().getName() + " ->OK"); } //2.創建對象(容器) // private static LazySingletonWithDCL lazyMan ; //5.new 不是一個原子性操作: private volatile static LazySingletonWithDCL lazyMan; //3.對外提供靜態實例化方法 //4.為保證線程安全,需要上鎖 public static LazySingletonWithDCL getInstance() { if (lazyMan == null) { synchronized (LazySingletonWithDCL.class) { if (lazyMan == null) { lazyMan = new LazySingletonWithDCL(); } } } return lazyMan; } public static void main(String[] args) throws Exception { LazySingletonWithDCL instance = LazySingletonWithDCL.getInstance(); System.out.println(instance); //創建成功: LazySingletonWithDCL@61bbe9ba //利用反射創建對象 //獲取無參構造 Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null); //開放權限 declaredConstructor.setAccessible(true); LazySingletonWithDCL instance_withReflect = declaredConstructor.newInstance(); System.out.println(instance_withReflect); //LazySingletonWithDCL@610455d6 System.out.println(instance == instance_withReflect); //false } }
main ->OK com.kuangstudy.Singleton.LazySingletonWithDCL@61bbe9ba Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.kuangstudy.Singleton.LazySingletonWithDCL.main(LazySingletonWithDCL.java:60) Caused by: java.lang.RuntimeException: 不要試圖通過反射破解單例 at com.kuangstudy.Singleton.LazySingletonWithDCL.<init>(LazySingletonWithDCL.java:20) ... 5 more
只創建出了一個實例,並成功攔截了 通過反射創建對象的行為
反射再扳回一城
上述例子2中是用一個餓漢+反射創建,但如果兩個對象都是用反射創建呢?
public static void main(String[] args) throws Exception { //獲取類模板class Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null); //開放權限 declaredConstructor.setAccessible(true); //通過空參創建實例 LazySingletonWithDCL instance_withReflect1 = declaredConstructor.newInstance(); LazySingletonWithDCL instance_withReflect2 = declaredConstructor.newInstance(); System.out.println(instance_withReflect1);//LazySingletonWithDCL@61bbe9ba System.out.println(instance_withReflect2);//LazySingletonWithDCL@610455d6 System.out.println(instance_withReflect1 == instance_withReflect2);//false }
結果非常明顯:再一次破壞了單例模式
懶漢單例再度防守:
(加入標志位)
在空參方法里運用標志位,因為實例化對象需要獲取類模板:
- 當獲取了一次類模板之后,就把標志位flag置反。
- 等一次通過反射獲取類模板創建對象的時候便能拋異常
public class LazySingletonWithDCL { //7.加入標志位,防止多個反射破壞單例 private static boolean flag = true; //1.私有化構造函數 //6.增加對反射的判斷 private LazySingletonWithDCL() { synchronized (LazySingletonWithDCL.class){ if (flag){ flag = false; }else { // //6.1如果此時已經有實例,阻止反射創建 // if (lazyMan != null){ throw new RuntimeException("不要試圖通過反射破解單例"); // } } } System.out.println(Thread.currentThread().getName() + " ->OK"); }
//2.創建對象(容器)
// private static LazySingletonWithDCL lazyMan ; //5.new 不是一個原子性操作: private volatile static LazySingletonWithDCL lazyMan; //3.對外提供靜態實例化方法 //4.為保證線程安全,需要上鎖 public static LazySingletonWithDCL getInstance() { if (lazyMan == null) { synchronized (LazySingletonWithDCL.class) { if (lazyMan == null) { lazyMan = new LazySingletonWithDCL(); } } } return lazyMan; } public static void main(String[] args) throws Exception { //獲取類模板class Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null); //開放權限 declaredConstructor.setAccessible(true); //通過空參創建實例 LazySingletonWithDCL instance_withReflect1 = declaredConstructor.newInstance(); System.out.println(instance_withReflect1);//LazySingletonWithDCL@61bbe9ba LazySingletonWithDCL instance_withReflect2 = declaredConstructor.newInstance(); System.out.println(instance_withReflect2); //報錯:java.lang.RuntimeException: 不要試圖通過反射破解單例 System.out.println(instance_withReflect1 == instance_withReflect2); } }
由運行結果可見,保住了單例模式。
至此,用過反射調用空參實例化的方法被標志位掐斷
反射再度進攻
(你既然設置了標志位,那我就來破壞標志位)
public static void main(String[] args) throws Exception{ //獲取標志位 Field flag = LazySingletonWithDCL.class.getDeclaredField("flag"); flag.setAccessible(true); //獲取類模板class Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null); //開放權限 declaredConstructor.setAccessible(true); //通過空參創建實例 LazySingletonWithDCL instance_withReflect1 = declaredConstructor.newInstance(); System.out.println(instance_withReflect1);//LazySingletonWithDCL@61bbe9ba //在獲取並使用完類模板后,重新設置flag值: flag.set(instance_withReflect1,true); LazySingletonWithDCL instance_withReflect2 = declaredConstructor.newInstance(); System.out.println(instance_withReflect2); }
此時:
main ->OK
com.kuangstudy.Singleton.LazySingletonWithDCL@61bbe9ba
main ->OK
com.kuangstudy.Singleton.LazySingletonWithDCL@511d50c0
可以看到單例模式再一次被破壞了
該如何真正確保單例?
解鈴還須系鈴人,那我們就從單例入手分析一波:
查看反射中newInstance()方法

否則會報異常:
IllegalArgumentException("Cannot reflectively create enum objects")
1
枚舉保證單例:
構造枚舉嘗試單例模式:
public enum EnumSingleton { INSTANCE; public EnumSingleton getInstance(){ return INSTANCE; } } class Test{ public static void main(String[] args) { EnumSingleton instance1 = EnumSingleton.INSTANCE; EnumSingleton instance2 = EnumSingleton.INSTANCE; System.out.println(instance1.hashCode());//1639705018 System.out.println(instance2.hashCode());//1639705018 } }
簡單代碼創建一個實例,針不戳
嘗試用反射破壞枚舉的單例模式
public static void main(String[] args) throws Exception { //instance1正常獲取 EnumSingleton instance1 = EnumSingleton.INSTANCE; System.out.println(instance1); //INSTANCE //instance2通過反射獲取: //1.獲取其空參 Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(null); //2.打開權限: constructor.setAccessible(true); //3.實例化對象: EnumSingleton instance2 = constructor.newInstance(); System.out.println(instance2); }
結果:
- instance1 正常實例化
- 但是在通過反射的時候報了錯:

找不到EnumSingleton 的空參構造?
這就奇怪了。
idea查看class文件也是只有空參構造:

用javap -p -c .\EnumSingleton.class口令也是看到空參:

探究枚舉類的構造函數:
上述方法行不通后得運用更加專業的工具進行反編譯:
使用jad工具
使用命令:
.\jad.exe -sjava .\EnumSingleton.class
將class成功反編譯為Java文件:
// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://kpdus.tripod.com/jad.html // Decompiler options: packimports(3) // Source File Name: EnumSingleton.java package com.kuangstudy.Singleton; public final class EnumSingleton extends Enum { public static EnumSingleton[] values() { return (EnumSingleton[])$VALUES.clone(); } public static EnumSingleton valueOf(String name) { return (EnumSingleton)Enum.valueOf(com/kuangstudy/Singleton/EnumSingleton, name); } private EnumSingleton(String s, int i) { super(s, i); } public EnumSingleton getInstance() { return INSTANCE; } public static final EnumSingleton INSTANCE; private static final EnumSingleton $VALUES[]; static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); } }
此時我們發現枚舉類內部其實是一個有參的構造函數。
此時我們修改代碼:
//1.獲取其有參構造: Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
這時候我們就能如願得到想要的報錯了 (?怎么聽起來怪怪的)
INSTANCE Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at com.kuangstudy.Singleton.Test.main(EnumSingleton.java:40)
小結:
用枚舉構建單例模式:

單例模式在JDK中的應用:
在Runtime類中使用了單例模式(不過是俄漢式)上代碼:
public class Runtime { //2.類的內部創建對象 private static Runtime currentRuntime = new Runtime(); //3.開發對外的實例化方法 public static Runtime getRuntime() { return currentRuntime; } //1.私有化構造方法 private Runtime() {} .... }
java中枚舉類型的使用
Java 枚舉(enum) 詳解7種常見的用法
JDK1.5引入了新的類型——枚舉。在 Java 中它雖然算個“小”功能,卻給我的開發帶來了“大”方便。
web項目里實體類使用枚舉類型:
一般在該實體類的包下在新建一個enumeration包,把枚舉類型的類寫在enumeration包下,例如:
1 public enum Color {
2 RED, //紅色
3 BLUE, //藍色
4 GREEN //綠色
5 }
然后在實體類里引用這個枚舉類。
1 @Enumerated(value = EnumType.STRING) 2 @Column(name = "color") 3 @NotNull 4 private Color color;
注意:
(1)@Enumerated(value=EnumType.ORDINAL)采用枚舉類型的序號值與數據庫進行交互,
此時數據庫的數據類型需要是數值類型,例如在實際操作中
CatTest ct = new CatTest(); ct.setColor(Color.BLUE);
當我們將對象ct保存到數據庫中的時候,數據庫中存儲的數值是BLUE在Color枚舉
定義中的序號1(序號從零開始);
(2)@Enumerated(value=EnumType.STRING)采用枚舉類型與數據庫進行交互,
此時數據庫的數據類型需要是NVACHAR2等字符串類型,例如在實際操作中
CatTest ct = new CatTest(); ct.setColor(Color.BLUR);
數據庫中存儲的數值是BLUE字符串。
枚舉類型對象之間的值比較,是可以使用==,直接來比較值,是否相等的,不是必須使用equals方法的喲。
用法一:常量
在JDK1.5 之前,我們定義常量都是: public static fianl.... 。現在好了,有了枚舉,可以把相關的常量分組到一個枚舉類型里,而且枚舉提供了比常量更多的方法。
1 public enum Color {
2 RED, GREEN, BLANK, YELLOW
3 }
用法二:switch
1 enum Signal {
2 GREEN, YELLOW, RED
3 }
4 public class TrafficLight {
5 Signal color = Signal.RED;
6 public void change() {
7 switch (color) {
8 case RED:
9 color = Signal.GREEN;
10 break;
11 case YELLOW:
12 color = Signal.RED;
13 break;
14 case GREEN:
15 color = Signal.YELLOW;
16 break;
17 }
18 }
19 }
用法三:向枚舉中添加新方法
1 public enum Color {
2 RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);
3 // 成員變量
4 private String name;
5 private int index;
6 // 構造方法
7 private Color(String name, int index) {
8 this.name = name;
9 this.index = index;
10 }
11 // 普通方法
12 public static String getName(int index) {
13 for (Color c : Color.values()) {
14 if (c.getIndex() == index) {
15 return c.name;
16 }
17 }
18 return null;
19 }
20 // get set 方法
21 public String getName() {
22 return name;
23 }
24 public void setName(String name) {
25 this.name = name;
26 }
27 public int getIndex() {
28 return index;
29 }
30 public void setIndex(int index) {
31 this.index = index;
32 }
33 }
用法四:覆蓋枚舉的方法
下面給出一個toString()方法覆蓋的例子。
1 public enum Color {
2 RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);
3 // 成員變量
4 private String name;
5 private int index;
6 // 構造方法
7 private Color(String name, int index) {
8 this.name = name;
9 this.index = index;
10 }
11 //覆蓋方法
12 @Override
13 public String toString() {
14 return this.index+"_"+this.name;
15 }
16 }
用法五:實現接口
所有的枚舉都繼承自java.lang.Enum類。由於Java 不支持多繼承,所以枚舉對象不能再繼承其他類。
1 public interface Behaviour {
2 void print();
3 String getInfo();
4 }
5 public enum Color implements Behaviour{
6 RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);
7 // 成員變量
8 private String name;
9 private int index;
10 // 構造方法
11 private Color(String name, int index) {
12 this.name = name;
13 this.index = index;
14 }
15 //接口方法
16 @Override
17 public String getInfo() {
18 return this.name;
19 }
20 //接口方法
21 @Override
22 public void print() {
23 System.out.println(this.index+":"+this.name);
24 }
25 }
用法六:使用接口組織枚舉
1 public interface Food {
2 enum Coffee implements Food{
3 BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO
4 }
5 enum Dessert implements Food{
6 FRUIT, CAKE, GELATO
7 }
8 }
用法七:關於枚舉集合的使用
java.util.EnumSet和java.util.EnumMap是兩個枚舉集合。EnumSet保證集合中的元素不重復;EnumMap中的 key是enum類型,而value則可以是任意類型。關於這個兩個集合的使用就不在這里贅述,可以參考JDK文檔。
關於枚舉的實現細節和原理請參考:
參考資料:《ThinkingInJava》第四版 http://softbeta.iteye.com/blog/1185573
枚舉類實現單例
enum SomeThing { INSTANCE; private Resource instance; SomeThing() { instance = new Resource(); } public Resource getInstance() { return instance; } public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //單例實現了安全 Resource instance = SomeThing.INSTANCE.getInstance(); Resource instance2 = SomeThing.INSTANCE.getInstance(); System.out.println(instance2.equals(instance2)); //反射獲取實例,失敗 Constructor<SomeThing> declaredConstructor = SomeThing.class.getDeclaredConstructor(String.class,int.class); declaredConstructor.setAccessible(true); SomeThing someThing = declaredConstructor.newInstance(); System.out.println(someThing); /* Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at com.bupt.SomeThing.main(TestEnum.java:141) */ } }


