真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

單例模式的介紹和用法

本篇內(nèi)容主要講解“單例模式的介紹和用法”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“單例模式的介紹和用法”吧!

創(chuàng)新互聯(lián)堅持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:做網(wǎng)站、成都做網(wǎng)站、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時代的富寧網(wǎng)站設(shè)計、移動媒體設(shè)計的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!

問題

1、說說單例模式的特點?

2、你知道單例模式的具體使用場景嗎?

3、單例模式常見寫法有幾種?

4、怎么樣保證線程安全?

5、怎么不會被反射攻擊?

6、怎樣保證不會被序列化和反序列化的攻擊?

7、枚舉為什么會不會被序列化?

定義

單例模式(Singleton Pattern)是 Java  中最簡單的設(shè)計模式之一。這種類型的設(shè)計模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對象的最佳方式。

這種模式涉及到一個單一的類,該類負(fù)責(zé)創(chuàng)建自己的對象,同時確保只有單個對象被創(chuàng)建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

特點:

  • 1、單例類只能有一個實例。

  • 2、單例類必須自己創(chuàng)建自己的唯一實例。

  • 3、單例類必須給所有其他對象提供這一實例

  • 4、隱藏所有的構(gòu)造方法

**目的:**保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。

案例:一家企業(yè)只能有一個CEO,有多個了其實亂套了。

使用場景

需要確保任何情況下都絕對只有一個實例。

比如:ServletContext、ServletConfig、ApplicationContext、DBTool等,都使用到了單列模式。

單例模式的寫法

  • 餓漢式

  • 懶漢式(包含雙重檢查鎖、靜態(tài)內(nèi)部類)

  • 注冊式(以枚舉為例)

餓漢式

從名字上就能看出,餓漢:餓了就得先吃飽,所以,一開始就搞定了。

餓漢式主要是使用了static,餓漢式也有兩種寫法,但本質(zhì)可以理解為是一樣的。

public class HungrySingleton{      private static final HungrySingleton INSTANCE;     static {         INSTANCE=new HungrySingleton();     } //    private static final HungrySingleton INSTANCE=new HungrySingleton();     private HungrySingleton(){      }      public static HungrySingleton getInstance(){         return INSTANCE;     } }

餓漢式有個致命的缺點:浪費空間,不需要也實例化。如果是成千上萬個,也這么玩,想想有多恐怖。

于是,就會想到,能不能在使用的時候在實例化,從而引出了懶漢式。

懶漢式

顧名思義,就是需要的時候再創(chuàng)建,因為懶,你不調(diào)用我方法,我是不會干活的。

下面是懶漢式的Java代碼實現(xiàn):

public class LazySingleton {      private static LazySingleton lazySingleton = null;      private LazySingleton() {     }      public static LazySingleton getInstance() {         if (lazySingleton == null) {//01             lazySingleton = new LazySingleton();//02         }         return lazySingleton;     }  }

進(jìn)入getInstance方法,先判斷l(xiāng)azySingleton是否為空,為空,則創(chuàng)建一個對象,然后返回此對象。

但是,問題來了:

兩個線程同時進(jìn)入getInstance方法,然后都去執(zhí)行01這行代碼,都是true,然后各自進(jìn)去創(chuàng)建一個對象,然后返回自己創(chuàng)建的對象。

這豈不是不滿足只有唯一 一個對象的了嗎?所以這類存在線程安全的問題,那怎么解決呢?

第一印象肯定都是想到加鎖。于是,就有了下面的線程安全的懶加載版本:

public class LazySingleton {      private static LazySingleton lazySingleton = null;      private LazySingleton() {     }      //簡單粗暴的線程安全問題解決方案     //依然存在性能問題   public synchronized static LazySingleton getInstance() {         if (lazySingleton == null) {             lazySingleton = new LazySingleton();         }         return lazySingleton;     } }

給getInstance方法加鎖同步鎖標(biāo)志synchronized,但是又涉及到鎖的問題了,同步鎖是對系統(tǒng)性能優(yōu)影響的,盡管JDK1.6后,對其做了優(yōu)化,但它畢竟還是涉及到鎖的開銷。

每個線程調(diào)用getInstance方法時候,都會涉及到鎖,所以又對此進(jìn)行了優(yōu)化成為了大家耳熟能詳?shù)碾p重檢查鎖。

雙重檢查鎖

代碼實現(xiàn)如下:

public class LazyDoubleCheckSingleton {      private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;      private LazyDoubleCheckSingleton() {     }      public static LazyDoubleCheckSingleton getInstance() {         if (lazyDoubleCheckSingleton == null) {//01             synchronized (LazyDoubleCheckSingleton.class) {                 if (lazyDoubleCheckSingleton == null) {//02                     lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();                 }             }         }         return lazyDoubleCheckSingleton;     }  }

這段代碼中,在01行,如果不為空,就直接返回,這是第一次檢查。如果為空,則進(jìn)入同步代碼塊,02行又進(jìn)行一次檢查。

雙重檢查就是現(xiàn)實if判斷、獲取類對象鎖、if判斷。

上面這段代碼,看似沒問題,其實還是有問題的,比如:指令重排序(需要有JVM知識墊底哈)

指令重排是什么意思呢?

比如java中簡單的一句

lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();

會被編譯器編譯成如下JVM指令:

memory =allocate(); //1:分配對象的內(nèi)存空間

ctorInstance(memory); //2:初始化對象

instance =memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址

但是這些指令順序并非一成不變,有可能會經(jīng)過JVM和CPU的優(yōu)化,指令重排成下面的順序:

memory =allocate(); //1:分配對象的內(nèi)存空間

instance =memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址

ctorInstance(memory); //2:初始化對象

為了防止指令重排序,所以,我們可以使用volatile來做文章(注意:volatile能防止指令重排序和線程可見性)。

于是,更好的版本就出來了。

public class LazyDoubleCheckSingleton {     //使用volatile修飾     private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;      private LazyDoubleCheckSingleton() {     }      public static LazyDoubleCheckSingleton getInstance() {         if (lazyDoubleCheckSingleton == null) {             synchronized (LazyDoubleCheckSingleton.class) {                 if (lazyDoubleCheckSingleton == null) {                     lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();                 }             }         }         return lazyDoubleCheckSingleton;     } }

盡管相比前面的版本,確實改進(jìn)了很多,但依然有同步鎖,還是會影響性能問題。于是,又進(jìn)行優(yōu)化為靜態(tài)內(nèi)部類方式:

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

下面是靜態(tài)內(nèi)部類的代碼實現(xiàn):

利用了內(nèi)部類的特性,在JVM底層,能完美的規(guī)避了線程安全的問題,這種方式也是目前很多項目里喜歡使用的方式。

但是,還是會存在潛在的風(fēng)險,什么風(fēng)險呢?

可以使用 反射 暴力的串改,同樣也會出現(xiàn)創(chuàng)建多個實例:

反射代碼實現(xiàn)如下:

import java.lang.reflect.Constructor;  public class LazyStaticSingletonTest {     public static void main(String[] args) {         try {             Class clazz = LazyStaticSingleton.class;             Constructor constructor = clazz.getDeclaredConstructor(null);             //強(qiáng)行訪問             constructor.setAccessible(true);             Object object = constructor.newInstance();              Object object1 = LazyStaticSingleton.getInstance();              System.out.println(object == object1);         } catch (Exception ex) {             ex.printStackTrace();         }     } }

這段代碼運行結(jié)果為false。

所以,上面說的雙重檢查鎖的方式,通過反射,還是會存在潛在的風(fēng)險。怎么辦呢?

在《Effect java 》這本書中,作者推薦使用枚舉來實現(xiàn)單例模式,因為枚舉不能被反射。

枚舉

下面是枚舉式的單例模式的代碼實現(xiàn):

public enum EnumSingleton {     INSTANCE;     private Object data;      public Object getData() {         return data;     }      public static EnumSingleton getInstance(){         return INSTANCE;     } }

我們把上面反射的那個代碼,來測試這個枚舉式單例模式。

public class EnumTest {     public static void main(String[] args) {         try {             Class clazz = EnumSingleton.class;             Constructor constructor = clazz.getDeclaredConstructor(null);             //強(qiáng)行訪問             constructor.setAccessible(true);             Object object = constructor.newInstance();              Object object1 = EnumSingleton.getInstance();              System.out.println(object == object1);         } catch (Exception ex) {             ex.printStackTrace();         }     } }

運行這段代碼:

java.lang.NoSuchMethodException: com.tian.my_code.test.designpattern.singleton.EnumSingleton.()  at java.lang.Class.getConstructor0(Class.java:3082)  at java.lang.Class.getDeclaredConstructor(Class.java:2178)  at com.tian.my_code.test.designpattern.singleton.EnumTest.main(EnumTest.java:41)

還真的不能用反射來搞。如果此時面試官,為什么枚舉不能被反射呢?

為什么枚舉不能被反射呢?

我們在反射的代碼中

Constructor constructor = clazz.getDeclaredConstructor(null);

這行代碼是獲取他的無參構(gòu)造方法。并且,從錯誤日志中,我們也可以看到,錯誤出現(xiàn)就是在getConstructor0方法中,并且,提示的是沒有找到無參構(gòu)造方法。

很奇怪,枚舉也是類,不是說如果我們不給類顯示定義構(gòu)造方法時候,會默認(rèn)給我們創(chuàng)建一個無參構(gòu)造方法嗎?

于是,我想到了一個辦法,我們可以使用jad這個工具去反編譯的我們的枚舉式單例的.class文件。

找到我們的class文件所在目錄,然后我們可以執(zhí)行下面這個命令:

C:\Users\Administrator>jad D:\workspace\my_code\other-local-demo\target\classes com\tian\my_code\test\designpattern\singleton\EnumSingleton.class Parsing D:\workspace\my_code\other-local-demo\target\classes\com\tian\my_code\t st\designpattern\singleton\EnumSingleton.class... Generating EnumSingleton.jad

注意:class文件目錄以及生成的jad文件所在的目錄。

然后打開EnumSingleton.jad 文件:

單例模式的介紹和用法

于是,我就想到了,那我們使用有參構(gòu)造方法來創(chuàng)建:

public class EnumTest {     public static void main(String[] args) {         try {             Class clazz = EnumSingleton.class;              Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);             //強(qiáng)行訪問             constructor.setAccessible(true);             Object object = constructor.newInstance("田維常",996);              Object object1 = EnumSingleton.getInstance();              System.out.println(object == object1);         } catch (Exception ex) {             ex.printStackTrace();         }     } }

再次運行這段代碼,結(jié)果:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects  at java.lang.reflect.Constructor.newInstance(Constructor.java:417)  at com.tian.my_code.test.designpattern.singleton.EnumTest.main(EnumTest.java:45)

提示很明顯了,就是不讓我們使用反射的方式創(chuàng)建枚舉對象。

public T newInstance(Object ... initargs)      throws InstantiationException, IllegalAccessException,             IllegalArgumentException, InvocationTargetException  {      if (!override) {          if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {              Class caller = Reflection.getCallerClass();              checkAccess(caller, clazz, null, modifiers);          }      }      //Modifier.ENUM就是用來判斷是否為枚舉的      if ((clazz.getModifiers() & Modifier.ENUM) != 0)          throw new IllegalArgumentException("Cannot reflectively create enum objects");      ConstructorAccessor ca = constructorAccessor;   // read volatile      if (ca == null) {          ca = acquireConstructorAccessor();      }      @SuppressWarnings("unchecked")      T inst = (T) ca.newInstance(initargs);      return inst;  }

所以,到此,我們才算真正的理清楚了,為什么枚舉不讓反射的原因。

序列化破壞

我們以非線程安全的餓漢式來演示一下,看看序列化是如何破壞到了模式的。

public class ReflectTest {      public static void main(String[] args) {         // 準(zhǔn)備兩個對象,singleton1接收從輸入流中反序列化的實例         HungrySingleton singleton1 = null;         HungrySingleton singleton2 = HungrySingleton.getInstance();         try {             // 序列化             FileOutputStream fos = new FileOutputStream("HungrySingleton.txt");             ObjectOutputStream oos = new ObjectOutputStream(fos);             oos.writeObject(singleton2);             oos.flush();             oos.close();              // 反序列化             FileInputStream fis = new FileInputStream("HungrySingleton.txt");             ObjectInputStream ois = new ObjectInputStream(fis);             singleton1 = (HungrySingleton) ois.readObject();             ois.close();              System.out.println(singleton1);             System.out.println(singleton2);                          System.out.println(singleton1 == singleton2);          } catch (Exception e) {             e.printStackTrace();         }     } }

運行結(jié)果:

com.tian.my_code.test.designpattern.singleton.HungrySingleton@7e6cbb7a com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41 false

看到了嗎?

使用序列化是可以破壞到了模式的,這種方式,可能很多人不是很清楚。

如何防止呢?

我們對非線程安全的餓漢式代碼進(jìn)行稍微修改:

public class HungrySingleton implements Serializable{      private static final HungrySingleton INSTANCE;     static {         INSTANCE=new HungrySingleton();     }      private HungrySingleton(){      }      public static HungrySingleton getInstance(){         return INSTANCE;     }     //添加了readResolve方法,并返回INSTANCE     private Object readResolve方法,并返回(){         return INSTANCE;     } }

再次運行上那段序列化測試的代碼,其結(jié)果如下:

com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41 com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41 true

嘿嘿,這樣我們是不是就避免了只創(chuàng)建了一個實例?

答案:否

在類ObjectInputStream的readObject()方法中調(diào)用了另外一個方法readObject0(false)方法。在readObject0(false)方法中調(diào)用了checkResolve(readOrdinaryObject(unshared))方法。

在readOrdinaryObject方法中有這么一段代碼:

Object obj; try {       //是否有構(gòu)造方法,有構(gòu)造放就創(chuàng)建實例       obj = desc.isInstantiable() ? desc.newInstance() : null;  } catch (Exception ex) {  ...   } //判斷單例類是否有readResolve方法 if (desc.hasReadResolveMethod()) {     Object rep = desc.invokeReadResolve(obj);  }  //invokeReadResolve方法中 if (readResolveMethod != null) {      //調(diào)用了我們單例類中的readResolve,并返回該方法返回的對象     //注意:是無參方法      return readResolveMethod.invoke(obj, (Object[]) null); }

繞了半天,原來他是這么玩的,上來就先創(chuàng)建一個實例,然后再去檢查我們的單例類是否有readResolve無參方法,我們單例類中的readResolve方法

private Object readResolve(){         return INSTANCE; }

結(jié)論

我們重寫了readResolve()無參方法,表面上看是只創(chuàng)建了一個實例,其實只創(chuàng)建了兩個實例。

緊接著,面試官繼續(xù)問:枚舉式單例能不能被序列化破壞呢?

枚舉式單例能不能被序列化破壞呢?

答案:不能被破壞,請看我慢慢給你道來。

don't talk ,show me the code。

我們先來驗證一下是否真的不能被破壞,請看代碼:

public class EnumTest {      public static void main(String[] args) {         // 準(zhǔn)備兩個對象,singleton1接收從輸入流中反序列化的實例         EnumSingleton singleton1 = null;         EnumSingleton singleton2 = EnumSingleton.getInstance();         try {             // 序列化             FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");             ObjectOutputStream oos = new ObjectOutputStream(fos);             oos.writeObject(singleton2);             oos.flush();             oos.close();              // 反序列化             FileInputStream fis = new FileInputStream("EnumSingleton.obj");             ObjectInputStream ois = new ObjectInputStream(fis);             singleton1 = (EnumSingleton) ois.readObject();             ois.close();              System.out.println(singleton1);             System.out.println(singleton2);              System.out.println(singleton1 == singleton2);          } catch (Exception e) {             e.printStackTrace();         }     } }

運行結(jié)果:

INSTANCE INSTANCE true

確實,枚舉式單例是不會被序列化所破壞,那為什么呢?總得有個證件理由吧。

在類ObjectInputStream的readObject()方法中調(diào)用了另外一個方法readObject0(false)方法。在readObject0(false)方法中調(diào)用了checkResolve(readOrdinaryObject(unshared))方法。

case TC_ENUM:    return checkResolve(readEnum(unshared));

在readEnum方法中

private Enum readEnum(boolean unshared) throws IOException {         if (bin.readByte() != TC_ENUM) {             throw new InternalError();         }         Class cl = desc.forClass();         if (cl != null) {             try {                 @SuppressWarnings("unchecked")                 //重點                 Enum en = Enum.valueOf((Class)cl, name);                 result = en;                 //...其他代碼省略             }         } } public static > T valueOf(Class enumType,                                                 String name) {        //enumType.enumConstantDirectory()返回的是一個HashMap        //通過HashMap的get方法獲取         T result = enumType.enumConstantDirectory().get(name);         if (result != null)             return result;         if (name == null)             throw new NullPointerException("Name is null");         throw new IllegalArgumentException(             "No enum constant " + enumType.getCanonicalName() + "." + name); } //返回一個HashMap  Map enumConstantDirectory() {         if (enumConstantDirectory == null) {             T[] universe = getEnumConstantsShared();             if (universe == null)                 throw new IllegalArgumentException(                     getName() + " is not an enum type");             //使用的是HashMap             Map m = new HashMap<>(2 * universe.length);             for (T constant : universe)                 m.put(((Enum)constant).name(), constant);             enumConstantDirectory = m;         }         return enumConstantDirectory; }

所以,枚舉式單例模式是使用了Map

在Spring中也是有大量使用這種注冊式單例模式,IOC容器就是典型的代表。

總結(jié)

本文講述了單例模式的定義、單例模式常規(guī)寫法。單例模式線程安全問題的解決,反射破壞、反序列化破壞等。

注意:不要為了套用設(shè)計模式,而使用設(shè)計模式。而是要,在業(yè)務(wù)上遇到問題時,很自然地聯(lián)想單設(shè)計模式作為一種捷徑方法。

單例模式的優(yōu)缺點

優(yōu)點

在內(nèi)存中只有一個實例,減少內(nèi)存開銷??梢员苊鈱Y源的多重占用。設(shè)置全局訪問點,嚴(yán)格控制訪問。

缺點

沒有借口,擴(kuò)展性很差。如果要擴(kuò)展單例對象,只有修改代碼,沒有其他途徑。

單例模式是 不符合開閉原則的。

到此,相信大家對“單例模式的介紹和用法”有了更深的了解,不妨來實際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!


標(biāo)題名稱:單例模式的介紹和用法
轉(zhuǎn)載來于:http://weahome.cn/article/gjsoje.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部