單例模式存在的問題
1.反射可以破解單例模式。
2.反序列化可以破解單例模式。
注:上面的單例模式不包括枚舉實現單例模式。
反射破解單例模式
我們以懶漢式爲例。
public class SingletonDemo06 {
private static SingletonDemo06 instance;
private SingletonDemo06(){
/*if (instance != null){
throw new RuntimeException();
}*/
}
public static synchronized SingletonDemo06 getInstance(){
if (instance == null){
instance = new SingletonDemo06();
}
return instance;
}
}
public class Client2 {
public static void main(String[] args) throws Exception {
SingletonDemo06 s1 = SingletonDemo06.getInstance();
SingletonDemo06 s2 = SingletonDemo06.getInstance();
System.out.println(s1);
System.out.println(s2);
Class<SingletonDemo06> clazz =
(Class<SingletonDemo06>) Class.forName("com.sxt.singleton.SingletonDemo06");
Constructor<SingletonDemo06> c = clazz.getDeclaredConstructor(null);
//c.setAccessible(true);
SingletonDemo06 s3 = c.newInstance();
SingletonDemo06 s4 = c.newInstance();
System.out.println(s3);
System.out.println(s4);
}
}
運行上面的Client2,我們可以看到以下報錯信息:
Client2無法訪問SingletonDemo06的私有成員。
我們將Client2中的 c.setAccessible(true); 這行代碼的註釋解開,再次運行Client2,可以看到以下運行結果:
可以看到上s3和s4不是同一個對象。我們跳過了單例模式,new了兩個對象。
如何防止反射破解單例模式呢?通過在構造器中拋出異常的方法來實現,將SingletonDemo06中的註釋解開,然後再運行Client2,將會拋出異常,可以看到,s3和s4沒有創建成功:
結論:可以在構造方法中手動拋出異常,來避免反射破解單例模式。
反序列化可以破解單例模式
SingletonDemo06需要實現序列化的接口。
public class SingletonDemo06 implements Serializable {
private static SingletonDemo06 instance;
//私有化構造器
private SingletonDemo06(){
if (instance != null){
throw new RuntimeException();
}
}
public static synchronized SingletonDemo06 getInstance(){
if (instance == null){
instance = new SingletonDemo06();
}
return instance;
}
/*private Object readResolve() throws ObjectStreamException{
return instance;
}*/
}
然後使用反序列化來構造多個對象:
public class Client2 {
public static void main(String[] args) throws Exception {
SingletonDemo06 s1 = SingletonDemo06.getInstance();
SingletonDemo06 s2 = SingletonDemo06.getInstance();
System.out.println(s1);
System.out.println(s2);
FileOutputStream fos = new FileOutputStream("d:/a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
SingletonDemo06 s3 = (SingletonDemo06) ois.readObject();
System.out.println(s3);
}
}
運行結果如下:
可以看到,s1和s2爲同一對象,而s3爲新的對象。
可以通過定義readResolve()方法來防止獲得不同對象。將SingletonDemo06中的註釋解開,再執行Client2,可以看到,此時的s3和s1s2爲同一對象,沒有被新建。
結論:反序列化時,可以通過定義readResolve()方法來防止獲得不同對象,因爲反序列化時,如果定義了readResolve()則直接返回此方法指定的對象(實際是一種回調),而不需要單獨再創建新對象。
五種單例模式在多線程環境下的效率
public class Client3 {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
int threadNum = 10;
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for (int i=0; i<10; i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<1000000; i++){
Object o = SingletonDemo01.getInstance();
}
countDownLatch.countDown();
}
}) .start();
}
//main線程阻塞,直到計數器變爲0,纔會繼續往下執行
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("總耗時:" + (end-start));
}
}
這裏用到了一個類CountDownLatch:同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或者多個線程一直等待。
countDown():當前線程調用此方法,則計數減一(建議放在finally裏面執行)。
await():調用此方法會一直阻塞當前線程,直到計數器的值爲0。
執行Client3,依次用SingletonDemo01-06來進行測試,可得出以下結果(由快到慢):餓漢式、靜態內部類式、枚舉式、雙重檢測鎖式、懶漢式。其中,懶漢式的執行效率最低,跟其他四種不在一個數量級(比如餓漢式耗時20s,懶漢式則爲300s)。
以上爲單例模式的學習筆記,此文章爲尚學堂視頻的學習筆記+自己總結。