一、五種實作方式
-
餓漢式
public class HungrySingleton { private static HungrySingleton instance = new HungrySingleton(); private HungrySingleton(){} public static HungrySingleton getInstance(){ return instance; } }餓漢式單例模式:當加載這個類時就創建物件,以空間換時間,類的特性是在 JVM 中只加載一次,所以這個形式的單例模式是執行緒安全的,
-
懶漢式
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton(){} public static LazySingleton getInstance(){ if (instance == null){ instance = new LazySingleton(); } return instance; } }懶漢式單例模式:當需要創建物件時再創建,以時間換空間,這個形式的單例模式是執行緒不安全的,當有多個執行緒訪問 getInstance 方法,會創建多個物件,是執行緒不安全的,所以引入了第三種單例模式:Double Check 懶漢式
-
Double Check 懶漢式
public class ConcurrentLazySingleton { private static ConcurrentLazySingleton instance = null; private ConcurrentLazySingleton(){} public static ConcurrentLazySingleton getInstance(){ if (instance == null){ synchronized (ConcurrentLazySingleton.class){ if (instance == null){ instance = new ConcurrentLazySingleton(); // !!! } } } return instance; } }Double Check 的方式保證了當多個執行緒訪問 getInstance 方法時,同一時間只有一個執行緒可以訪問該方法,第一個 if 判斷避免了當物件已存在時,進行不必要的加鎖,第二個 if 判斷保證了單例物件只會存在一個,
但 DCL 方式的單例模式還是會有問題:以下這步創建物件的操作不是原子性的,
instance = new ConcurrentLazySingleton();創建物件在 JVM 中的執行分為了四個步驟
- 在堆記憶體中開辟記憶體空間
- 在堆記憶體中實體化物件的各個引數(賦零值)
- 將必要的資訊(類的元資料資訊,物件的hash值,物件的gc分代年齡等)存盤在物件的物件頭中
- 執行 init<> 方法,生產物件
因為 JVM 是亂序執行指令的,如果當創建單例物件的時候,剛剛執行完第一步,就執行了第四步,那么這時候 instance == null 已經為 false 了,如果此時下一個行程進入方法,就會直接將內部資訊為空的物件回傳,造成例外,
所以 Java 引入了 volatile 關鍵字
volatile 修飾的變數,可以防止 JVM 指令重排序,保證了有序性,并且這個變數直接在主記憶體中被所有執行緒共享,保證了可見性,
public class ConcurrentLazySingleton2 { private static volatile ConcurrentLazySingleton2 instance; private ConcurrentLazySingleton2(){} public static ConcurrentLazySingleton2 getInstance(){ if (instance == null){ synchronized (ConcurrentLazySingleton2.class){ if (instance == null){ instance = new ConcurrentLazySingleton2(); } } } return instance; } } -
靜態內部類式
public class StaticInnerSingleton { private StaticInnerSingleton(){} private static class Inner{ private static StaticInnerSingleton instance = new StaticInnerSingleton(); } public static StaticInnerSingleton getInstance(){ return Inner.instance; } }靜態內部類式單例模式:在類中封裝了一個私有的靜態內部類,當 StaticInnerSingleton 類被加載時,內部類并不會被加載,僅當內部類的靜態成員(靜態變數,靜態方法,構造器)被呼叫時,類才會被加載,并且只加載一次, 與餓漢式單例模式不同,這樣的方式延遲了物件的創建,僅當需要創建物件的時候才創建,節省了空間,與餓漢式單例模式相同,都保證了執行緒安全,
但這個形式也有缺陷,就是初始化時不能傳參進去,
-
列舉
public enum EnumSingleton { instance("test"); private String name; EnumSingleton(String test) { } }因為列舉中構造器默認為私有的,并且列舉實體創建時是執行緒安全的,所以保證了任一時刻只有一個單例物件,
二、反序列化破解單例模式
? 通過學習 Java 的序列化相關知識可以了解到,ObjectOutPutStream 可以將 A 物件保存為位元組陣列中,再由 ObjectInPutStream 將檔案中讀取出來還原為物件 B,A,B不相同,
? 那么單例物件提供序列化以及反序列化,會不會創建出第二個單例物件呢?
? 以下是測驗代碼:
public static void main(String[] args) {
SingletonDemo demo = SingletonDemo.getInstance();
demo.setTest("singletonTest");
System.out.println(demo);
ObjectOutputStream objectOutputStream = null;
objectOutputStream = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
objectOutputStream.writeObject(demo);
ObjectInputStream objectInputStream = null;
File file = new File("singleton.txt");
objectInputStream = new ObjectInputStream(new FileInputStream(file));
SingletonDemo instance1 =(SingletonDemo) objectInputStream.readObject();
System.out.println(instance1);
...為省篇幅,將例外的捕捉以及流的關閉省略
}
輸出為:
com.java_1217.singleton.SingletonDemo@6d6f6e28
com.java_1217.singleton.SingletonDemo@27d6c5e0
為什么可以破解?
? 因為實作了 Serializable 介面的單例類,通過反序列化創建物件時,呼叫的并不是單例類的構造方法,而是第一個非 Serializable 的無參構造方法,如果沒有相應的父類,最侄訓找到 Object 類,
public class User extends Person implements Serializable {
private transient String name;
private Cat age;
public User() {
}
}
public class Person extends Human implements Serializable {
public Person() {
System.out.println("呼叫了Person父類的無參構造");
}
}
public class Human {
public Human() {
System.out.println("呼叫了Human父類的無參構造");
}
}
//通過反序列化創建 User 物件,控制臺輸出為 "呼叫了Human父類的無參構造"
如何避免?
? Java 提供了名為 readResolve 的私有方法,當反序列化物件時,通過反射機制判斷被反序列化的類是否含有 readResolve 方法,如果有的話,回傳 readResolve 方法的回傳值,
public class SingletonDemo implements Serializable {
private static SingletonDemo instance;
private SingletonDemo(){
System.out.println("創建了單例物件");
}
public static SingletonDemo getInstance(){
if (instance == null){
synchronized (SingletonDemo.class){
if (instance == null){
instance = new SingletonDemo();
}
}
}
return instance;
}
private Object readResolve() throws ObjectStreamException{
return instance;
}
}
? 在單例類中加入以上方法,再次測驗,輸出為:
com.java_1217.singleton.SingletonDemo@6d6f6e28
com.java_1217.singleton.SingletonDemo@6d6f6e28
三、暴力反射破解單例模式
? 通過學習反射相關知識,我們知道可以通過暴力反射的方式獲取類中的私有化屬性、方法、以及構造器,已知單例模式將構造器私有化,那么是否可以通過暴力反射的方式,獲取被私有的構造器,并提供構造器來創建單例物件呢?
? 以下為測驗代碼:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
SingletonDemo instance = SingletonDemo.getInstance();
System.out.println(instance);
Class clazz = Class.forName("com.java_1217.singleton.SingletonDemo");
// Constructor constructor = clazz.getConstructor();
// 不能使用 getConstructor() 方法,因為 getConstructor() 方法只回傳 public 型別的構造器
// 而 getDeclaredConstructor() 方法可以回傳包括 public和 非public 型別的構造器
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonDemo newInstance = (SingletonDemo) constructor.newInstance(null);
System.out.println(newInstance);
}
//輸出為:
創建了單例物件
com.java_1217.singleton.SingletonDemo@6d6f6e28
創建了單例物件
com.java_1217.singleton.SingletonDemo@135fbaa4
如何避免?
? 可以在私有的構造方法中,加入判斷,
private SingletonDemo(){
if (instance != null){
throw new RuntimeException("不能訪問私有構造方法!");
}
System.out.println("創建了單例物件");
}
? 再次測驗結果為:
創建了單例物件
com.java_1217.singleton.SingletonDemo@6d6f6e28
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.java_1217.singleton.Test_reflect.main(Test_reflect.java:16)
Caused by: java.lang.RuntimeException: 不能訪問私有構造方法!
at com.java_1217.singleton.SingletonDemo.<init>(SingletonDemo.java:13)
... 5 more
Process finished with exit code 1
參考資料:靜態內部類單例原理
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/274047.html
標籤:java
上一篇:網路編程(TCP/IP)
下一篇:Maven配置詳解
