單例模式

懶漢式

public class LazySingleton{
    private LazySingleton instance;
    private LazySingleton{}//構(gòu)造方法私有化
    public static LazySingleton getInstance{
        if(instance == null){
            instance = new LazySingleton();
        }
    return instance;
    }
}

之所以稱之為“懶漢式”,是因?yàn)樵谛枰搶?shí)例的時(shí)候才會(huì)去進(jìn)行創(chuàng)建的操作。對(duì)于多線程開發(fā),上面的單例模式就不能夠保證該類對(duì)象是單例了,因?yàn)槎嗑€程可能會(huì)同時(shí)進(jìn)入if語(yǔ)句內(nèi),從而都會(huì)去創(chuàng)建這個(gè)實(shí)例。

public class LazySingleton{
    private LazySingleton instance;
    private LazySingleton(){};
    public static synchronized LazySingleton getInstance{
        if(instance == null){
            instance = new LazySingleton();
        }
    return instance;
    }
}

我們通過給getInstance方法添加synchronized關(guān)鍵字(也就是加鎖),保證該方法一次只能被一個(gè)線程調(diào)用,從而避免了上述的多線程破壞單例的情況。但是這個(gè)方法也存在缺點(diǎn),就是當(dāng)instance已經(jīng)被初始化后再調(diào)用時(shí)每次都需要去做加鎖的操作,非常影響效率。

public class LazySingleton{
    private LazySingleton instance;
    private LazySingleton(){};
    public static LazySingleton getInstance(){
        if(instance == null){
            synchronized(LazySingleton.class){
                instance = new LazySingleton();
            }
        }
    return instance;
    }
}

我們直接將鎖從方法挪到實(shí)例化對(duì)象中去了,這樣的話當(dāng)instance不等于空的時(shí)候不用處理鎖,直接進(jìn)行返回。但是這種寫法仍然會(huì)有問題,當(dāng)多個(gè)線程同時(shí)進(jìn)入if語(yǔ)句時(shí),都會(huì)排隊(duì)獲取鎖并進(jìn)行實(shí)例化對(duì)象,從而又破壞了單例模式。那我們應(yīng)該怎么做呢?

public class LazySingleton{
    private LazySingleton instance;
    private LazySingleton(){};
    public static LazySingleton getInstance(){
        if(instance == null){ // 注釋1
            synchronized(LazySingleton.class){
                if(instance == null){
                    instance = new LazySingleton();//注釋2
                }
            }
        }
    return instance;
    }
}

我們可以在實(shí)例化之前再做一次判斷,這樣第一次獲得所的線程實(shí)例化之后,后面獲得鎖的線程發(fā)現(xiàn)instance不為空,直接返回對(duì)象。但是這個(gè)方式仍然存在弊端,有可能會(huì)造成該對(duì)象不為空,但是不能被使用的情況。原因主要是因?yàn)閷?shí)例化對(duì)象的時(shí)候會(huì)有以下三個(gè)步驟:

  1. 在堆內(nèi)存中開辟空間;
  2. 初始化對(duì)象;
  3. 將該對(duì)象指向?qū)?yīng)的引用,引用賦值

而這3個(gè)步驟的順序不是固定的,有可能是1-2-3,也有可能是1-3-2,我們稱之為指令重排。那試想一下,第一個(gè)線程獲得鎖后,實(shí)例化對(duì)象的順序?yàn)?-3-2,當(dāng)剛執(zhí)行完3還沒有來得及執(zhí)行2的時(shí)候,第二個(gè)線程執(zhí)行第一個(gè)if語(yǔ)句,會(huì)發(fā)現(xiàn)instance此時(shí)不為空直接返回。但是這時(shí)instance還沒有被初始化,所以用instance進(jìn)行操作就會(huì)出現(xiàn)問題。那該如何解決呢,答案就是添加volatile關(guān)鍵字禁止這種指令重排,代碼如下:

public class LazySingleton{
    private volatile LazySingleton instance;
    private LazySingleton(){};
    public static LazySingleton getInstance(){
        if(instance == null){ // 注釋1
            synchronized(LazySingleton.class){
                if(instance == null){
                    instance = new LazySingleton();//注釋2
                }
            }
        }
    return instance;
    }
}

餓漢式

public class HungrySingleton{
    private static HungrySingleton instance = new HungrySingleton{};
    private HungrySingleton(){}
    public static HungrySingleton getInstance(){
        return instance;
    }
}

當(dāng)調(diào)用HungrySingleton類的getInstance方法時(shí),JVM將HungrySingleton類加載到內(nèi)存中,通過類加載機(jī)制確保instance的唯一性。但是餓漢式存在一個(gè)問題,就是當(dāng)類一旦被加載,instance實(shí)例就會(huì)被創(chuàng)建,會(huì)造成不必要的內(nèi)存開銷。

靜態(tài)內(nèi)部類

public class InnerClassSingleton{
    private static class InnerClassHolder{
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }
    private InnerClassSingleton(){}
    public static InnerClassSingleton getInstance(){
        return InnerClassHolder.instance;
    }
}

靜態(tài)內(nèi)部類的這個(gè)方式相當(dāng)于是結(jié)合了懶漢式和餓漢式兩者的優(yōu)點(diǎn)。通過餓漢式一樣的類加載機(jī)制實(shí)現(xiàn)對(duì)象的唯一性,程序只有在調(diào)用getInstance方法時(shí)才會(huì)去加載InnerClassHolder類從而創(chuàng)建實(shí)例,避免加載InnerClassSingleton類時(shí)就創(chuàng)建對(duì)象,造成不必要的內(nèi)存開銷。

反射攻擊

上面的餓漢式和靜態(tài)內(nèi)部類實(shí)現(xiàn)的單例模式碰到反射機(jī)制就變得無效了,具體代碼如下:

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Test1 {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objClass = InnerClassSingleton.class;
        //獲取類的構(gòu)造器
        Constructor constructor = objClass.getDeclaredConstructor();
        //把構(gòu)造器私有權(quán)限放開
        constructor.setAccessible(true);
        //正常的獲取實(shí)例方式
        InnerClassSingleton staticInnerClass = InnerClassSingleton.getInstance();
        //反射創(chuàng)建實(shí)例
        InnerClassSingleton newStaticInnerClass = (InnerClassSingleton) constructor.newInstance();

        System.out.println(staticInnerClass);
        System.out.println(newStaticInnerClass);
        System.out.println(staticInnerClass == newStaticInnerClass);

    }

}

上面的結(jié)果返回的是false,說明出現(xiàn)了兩個(gè)不同的實(shí)例,這就違反了我們的單例原則,不能保證只有一個(gè)實(shí)例了。那如何解決呢?

public class InnerClassSingleton{
    private static class InnerClassHolder{
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }
    private InnerClassSingleton(){
    if(InnerClassHolder.instance != null){
      throw IllegalStateException();//在這里判斷instance是否為空,不為空直接拋出異常
    }  
  }
  
    public static InnerClassSingleton getInstance(){
        return InnerClassHolder.instance;
    }
}

對(duì)于通過類加載時(shí)創(chuàng)建對(duì)象實(shí)例的這種單例模式,靜態(tài)內(nèi)部類、餓漢式,可以通過上面的方式來防止反射攻擊。反之如果不是通過類加載時(shí)創(chuàng)建實(shí)例的方式就沒有效果了。舉個(gè)例子:

public class LazySingleton{
    private volatile LazySingleton instance;
    private LazySingleton(){
    if(instance != null){
      throw IllegalStateException();
    }  
  };
    public static LazySingleton getInstance(){
        if(instance == null){ // 注釋1
            synchronized(LazySingleton.class){
                if(instance == null){
                    instance = new LazySingleton();//注釋2
                }
            }
        }
    return instance;
    }
}

其實(shí)很好理解了,假設(shè)我們先通過反射創(chuàng)建對(duì)象,走到構(gòu)造方法的時(shí)候instance對(duì)象為null,因此會(huì)正常創(chuàng)建對(duì)象并不會(huì)拋出異常。然后通過調(diào)用getInstance方法時(shí),instance會(huì)重新實(shí)例化一次,從而就出現(xiàn)了兩個(gè)不同的實(shí)例了。

其中對(duì)象的序列化也能夠破壞單例模式,具體代碼如下:

public class SerSingleton implements Serializable {
 2     private volatile static SerSingleton uniqueInstance;
 3     private  String content;
 4     public String getContent() {
 5         return content;
 6     }
 7 
 8     public void setContent(String content) {
 9         this.content = content;
10     }
11     private SerSingleton() {
12     }
13 
14     public static SerSingleton getInstance() {
15         if (uniqueInstance == null) {
16             synchronized (SerSingleton.class) {
17                 if (uniqueInstance == null) {
18                     uniqueInstance = new SerSingleton();
19                 }
20             }
21         }
22         return uniqueInstance;
23     }
24 
25     
26     public static void main(String[] args) throws IOException, ClassNotFoundException {
27         SerSingleton s = SerSingleton.getInstance();
28         s.setContent("單例序列化");
29         System.out.println("序列化前讀取其中的內(nèi)容:"+s.getContent());
30         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
31         oos.writeObject(s);
32         oos.flush();
33         oos.close();
34 
35         FileInputStream fis = new FileInputStream("SerSingleton.obj");
36         ObjectInputStream ois = new ObjectInputStream(fis);
37         SerSingleton s1 = (SerSingleton)ois.readObject();
38         ois.close();
39         System.out.println(s+"\n"+s1);
40         System.out.println("序列化后讀取其中的內(nèi)容:"+s1.getContent());
41         System.out.println("序列化前后兩個(gè)是否同一個(gè):"+(s==s1));
42     }
43     
44 }

得到的結(jié)果為false,是兩個(gè)不同的實(shí)例,因此通過序列化對(duì)象的方式會(huì)破壞單例模式。那有什么辦法可以即保證單例、避免反射攻擊又能避免序列化呢? 答案就是枚舉了,代碼如下:

public enum  SerEnumSingleton implements Serializable {
 2     INSTANCE;
 3     private  String content;
 4     public String getContent() {
 5         return content;
 6     }
 7     public void setContent(String content) {
 8         this.content = content;
 9     }
10     private SerEnumSingleton() {
11     }
12 
13     public static void main(String[] args) throws IOException, ClassNotFoundException {
14         SerEnumSingleton s = SerEnumSingleton.INSTANCE;
15         s.setContent("枚舉單例序列化");
16         System.out.println("枚舉序列化前讀取其中的內(nèi)容:"+s.getContent());
17         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
18         oos.writeObject(s);
19         oos.flush();
20         oos.close();
21 
22         FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
23         ObjectInputStream ois = new ObjectInputStream(fis);
24         SerEnumSingleton s1 = (SerEnumSingleton)ois.readObject();
25         ois.close();
26         System.out.println(s+"\n"+s1);
27         System.out.println("枚舉序列化后讀取其中的內(nèi)容:"+s1.getContent());
28         System.out.println("枚舉序列化前后兩個(gè)是否同一個(gè):"+(s==s1));
29     }
30 }

至此,我們常見的單例模式就說完了,包括餓漢式、懶漢式、靜態(tài)內(nèi)部類和枚舉4種方式。謝謝大家的支持?。。?/p>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容