- 基礎概念
單例模式就是只需要創建一次,在整個應用生命周期都可以一直使用。
我們常分為餓漢式和懶漢式兩種。
餓漢式
餓漢式是在初始化的時候就將單例對象創建出來。通常,通過屬性new創建自身。該方式不存在線程安全的問題(JVM保證線程安全),但會造成內存資源的浪費。
我們可以創建一個這樣的類:
1、定義私有化的成員變量:需初始化,用static修飾。
2、私有化構造器,防止其被其他類new。
3、對外提供公共方法,返回獲取創建好的單例對象,用static修飾。
public class Singleton { // 私有化的成員變量:需初始化 private static Singleton singleton = new Singleton(); // 私有化構造器,防止其被其他類new private Singleton() { } // 對外提供公共方法,返回獲取創建好的單例對象 public static Singleton getInstance() { return singleton; } public void otherMethod() { System.out.print("其他的行為方法"); } }
- 懶漢式
懶漢式是在第一次使用的時候,才將單例對象創建出來。該方式存在線程安全的問題,但不會造成內存資源的浪費。
我們可以創建一個這樣的類:
1、定義私有化的成員變量:無需初始化,用static修飾。
2、私有化構造器,防止其被其他類new。
3、對外提供公共方法,返回獲取創建好的單例對象。只有當不存在時候才new,存在則直接返回,用static修飾。
public class Singleton { // 私有化的成員變量:不做初始化 private static Singleton singleton = null; // 私有化構造器,防止其被其他類new private Singleton() { } // 對外提供公共方法,返回獲取創建好的單例對象 public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } public void otherMethod() { System.out.print("其他的行為方法"); } }
懶漢式,怎么解決線程安全問題呢?
- 雙重檢查鎖
兩個線程同時訪問的時候,我們可以加鎖處理。
1、私有化的成員變量:不做初始化,volatile 保證原子性。
2、私有化構造器,防止其被其他類new。
3、對外提供公共方法,返回獲取創建好的單例對象。加兩層非空校驗,將第二層校驗為null的代碼塊用synchronized同步代碼塊。
public class Singleton { // 私有化的成員變量:不做初始化 private volatile static Singleton singleton = null; // 私有化構造器,防止其被其他類new private Singleton() { } // 對外提供公共方法,返回獲取創建好的單例對象 public static Singleton getInstance() { // 第一層非空校驗 if (singleton == null) { // 加同步鎖,保證只有一個線程進入 synchronized (Singleton.class) { // 第二層非空校驗,防止在第一次非空校驗時,兩個線程拿到的都是null對象而創建兩次。 if (singleton == null) { singleton = new Singleton(); } } } return singleton; } public void otherMethod() { System.out.print("其他的行為方法"); } }
可以了解下對象在JVM中的創建步驟。以及線程相關知識點,同步,怎么保證原子性等。
- 靜態內部類
1、私有化構造器,防止其被其他類new。
2、使用內部類(JVM保證),創建單例對象。
3、對外提供公共方法,通過調用內部類的屬性,返回獲取的創建好的單例對象。
public class StaticSingleton { // 私有化構造器,防止其被其他類new private StaticSingleton() { } // 使用內部類(JVM保證),創建單例對象 private static class SingletonFactory { private static StaticSingleton singleton = new StaticSingleton(); } // 對外提供公共方法,通過調用內部類的屬性,返回獲取的創建好的單例對象 public static StaticSingleton getInstance() { return SingletonFactory.singleton; } public void otherMethod() { System.out.print("其他的行為方法"); } }
- 枚舉
public enum SingletonEnum { INSTANCE; public void otherMethod() { System.out.print("其他的行為方法"); } }
問題:使用上面的雙重檢查鎖方式,我們如何破壞單例?
先上代碼
/** * 單例攻擊 */ public class SingletonAttack { public static void main(String[] args) throws Exception { // Singleton2 singleton1 = Singleton2.getInstance(); // Singleton2 singleton2 = Singleton2.getInstance(); // System.out.println(singleton1 == singleton2); // true // reflectAttack(); // reflectAttackWithThread(); // serializationAttack(); } /** 反射攻擊測試 */ public static void reflectAttack() throws Exception { Singleton2 singleton1 = Singleton2.getInstance(); Constructor<Singleton2> constructor = Singleton2.class.getDeclaredConstructor(); // true-設置成員變量的暴力破解 constructor.setAccessible(true); // Singleton2 singleton1 = constructor.newInstance(null); Singleton2 singleton2 = constructor.newInstance(null); System.out.println(singleton1 == singleton2); } /** * 序列化深拷貝攻擊測試 * 如果重寫了readResolve()方法,則執行會報錯ClassCastException */ public static void serializationAttack() throws Exception { ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("objectFile")); Singleton2 singleton1 = Singleton2.getInstance(); os.writeObject(singleton1); os.close(); ObjectInputStream is = new ObjectInputStream(new FileInputStream(new File("objectFile"))); Singleton2 singleton2 = (Singleton2) is.readObject(); is.close(); System.out.println(singleton1 == singleton2); } /** 多線程下反射攻擊測試 */ public static void reflectAttackWithThread() throws Exception { for (int i=0; i<100; i++) { Thread thread = new Thread(new Runnable() { public void run() { try { Constructor<Singleton2> constructor = Singleton2.class.getDeclaredConstructor(); // true-設置成員變量的暴力破解 constructor.setAccessible(true); Singleton2 singleton = constructor.newInstance(null); System.out.println(singleton); } catch (Exception e) { // 此處方便測試,不打印異常,防止因為異常而終止循環,因為要保證所有線程都能執行 // 如果輸出多個不同實例,則表示破壞了單例 // 如果只輸出一個實例,表示其他線程都無法獲取實例對象,線程中途出現了異常 } } }); thread.start(); } } }
1.反射,通過反射獲取單例對象的構造器,暴力破解后即可創建多個不同實例。怎么防止:私有構造方法加雙重檢查鎖。
private volatile static boolean isFirstCreate = true; private Singleton() { // 這里雙重校驗,也是防止兩個線程拿到的都是true,而創建了兩個實例 if (isFirstCreate) { synchronized (Singleton.class) { if (isFirstCreate) { // 為第一次創建,將isFirstCreate設置為true isFirstCreate = false; } else { // isFirstCreate為true,表示之前創建過了,需要拋出異常 throw new RuntimeException("此單例對象已存在,禁止非法調用構造器!"); } } } else { // isFirstCreate為true,表示之前創建過了,需要拋出異常 throw new RuntimeException("此單例對象已存在,禁止非法調用構造器!"); } }
2.序列化,通過深克隆復制對象,可生成多個實例。怎么防止:重寫在單例對象中readObject()方法。
如果單例對象實現了Serializable接口,我們可以通過在單例對象中重寫readResolve(),禁止程序通過深拷貝創建多個實例,達到破壞單例對象的目的。
private Object readResolve() throws ObjectStreamException { return Singleton.class;
}
優化后的雙重檢查鎖方式的單例類:
public class Singleton2 implements Serializable { // 私有化的成員變量:不做初始化 private volatile static Singleton2 singleton = null; private volatile static boolean isFirstCreate = true; // 私有化構造器,防止其被其他類new private Singleton2() { // 這里雙重校驗,也是防止兩個線程拿到的都是true,而創建了兩個實例 if (isFirstCreate) { synchronized (Singleton2.class) { if (isFirstCreate) { // 為第一次創建,將isFirstCreate設置為true isFirstCreate = false; } else { // isFirstCreate為true,表示之前創建過了,需要拋出異常 throw new RuntimeException("此單例對象已存在,禁止非法調用構造器!"); } } } else { // isFirstCreate為true,表示之前創建過了,需要拋出異常 throw new RuntimeException("此單例對象已存在,禁止非法調用構造器!"); } } // 對外提供公共方法,返回獲取創建好的單例對象 public static Singleton2 getInstance() { // 第一層非空校驗 if (singleton == null) { // 加同步鎖,保證只有一個線程進入 synchronized (Singleton2.class) { // 第二層非空校驗,防止在第一次非空校驗時,兩個線程拿到的都是null對象而創建兩次。 if (singleton == null) { singleton = new Singleton2(); } } } return singleton; } public void otherMethod() { System.out.print("其他的行為方法"); } /** 單例對象實現了Serializable接口,通過重寫readResolve()禁止程序通過深拷貝創建多個實例,達到破壞單例對象的目的 */ private Object readResolve() throws ObjectStreamException { return Singleton2.class; } }