初級(jí)必備:?jiǎn)卫J降?個(gè)問(wèn)題
故事
實(shí)話實(shí)說(shuō),關(guān)于單例模式,網(wǎng)上有N多個(gè)版本。你估計(jì)也看過(guò)很多版本。但看完了又能怎樣?我技術(shù)群里的一位小伙伴,上周面試,就因?yàn)橐粋€(gè)單例模式,然后叫他回去等通知了。
下面是這位同學(xué)被問(wèn)到的問(wèn)題:
1、說(shuō)說(shuō)單例模式的特點(diǎn)?
2、你知道單例模式的具體使用場(chǎng)景嗎?
3、單例模式常見(jiàn)寫(xiě)法有幾種?
4、怎么樣保證線程安全?
5、怎么不會(huì)被反射攻擊?
6、怎樣保證不會(huì)被序列化和反序列化的攻擊?
7、枚舉為什么會(huì)不會(huì)被序列化?
.....
你也可以嘗試行的回答這幾個(gè)題,看看自己能回答上幾個(gè)。
定義
單例模式(Singleton Pattern)是 Java 中最簡(jiǎn)單的設(shè)計(jì)模式之一。這種類(lèi)型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。
這種模式涉及到一個(gè)單一的類(lèi),該類(lèi)負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類(lèi)提供了一種訪問(wèn)其唯一的對(duì)象的方式,可以直接訪問(wèn),不需要實(shí)例化該類(lèi)的對(duì)象。
特點(diǎn):
- 1、單例類(lèi)只能有一個(gè)實(shí)例。
- 2、單例類(lèi)必須自己創(chuàng)建自己的唯一實(shí)例。
- 3、單例類(lèi)必須給所有其他對(duì)象提供這一實(shí)例
- 4、隱藏所有的構(gòu)造方法
**目的:**保證一個(gè)類(lèi)僅有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)。
案例:一家企業(yè)只能有一個(gè)CEO,有多個(gè)了其實(shí)亂套了。
使用場(chǎng)景
需要確保任何情況下都絕對(duì)只有一個(gè)實(shí)例。
比如:ServletContext、ServletConfig、ApplicationContext、DBTool等,都使用到了單列模式。
單例模式的寫(xiě)法
- 餓漢式
- 懶漢式(包含雙重檢查鎖、靜態(tài)內(nèi)部類(lèi))
- 注冊(cè)式(以枚舉為例)
餓漢式
從名字上就能看出,餓漢:餓了就得先吃飽,所以,一開(kāi)始就搞定了。
餓漢式主要是使用了static,餓漢式也有兩種寫(xiě)法,但本質(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;
- }
- }
餓漢式有個(gè)致命的缺點(diǎn):浪費(fèi)空間,不需要也實(shí)例化。如果是成千上萬(wàn)個(gè),也這么玩,想想有多恐怖。
于是,就會(huì)想到,能不能在使用的時(shí)候在實(shí)例化,從而引出了懶漢式。
懶漢式
顧名思義,就是需要的時(shí)候再創(chuàng)建,因?yàn)閼?,你不調(diào)用我方法,我是不會(huì)干活的。
下面是懶漢式的Java代碼實(shí)現(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)建一個(gè)對(duì)象,然后返回此對(duì)象。
但是,問(wèn)題來(lái)了:
兩個(gè)線程同時(shí)進(jìn)入getInstance方法,然后都去執(zhí)行01這行代碼,都是true,然后各自進(jìn)去創(chuàng)建一個(gè)對(duì)象,然后返回自己創(chuàng)建的對(duì)象。
這豈不是不滿(mǎn)足只有唯一 一個(gè)對(duì)象的了嗎?所以這類(lèi)存在線程安全的問(wèn)題,那怎么解決呢?
第一印象肯定都是想到加鎖。于是,就有了下面的線程安全的懶加載版本:
- public class LazySingleton {
- private static LazySingleton lazySingleton = null;
- private LazySingleton() {
- }
- //簡(jiǎn)單粗暴的線程安全問(wèn)題解決方案
- //依然存在性能問(wèn)題
- public synchronized static LazySingleton getInstance() {
- if (lazySingleton == null) {
- lazySingleton = new LazySingleton();
- }
- return lazySingleton;
- }
- }
給getInstance方法加鎖同步鎖標(biāo)志synchronized,但是又涉及到鎖的問(wèn)題了,同步鎖是對(duì)系統(tǒng)性能優(yōu)影響的,盡管JDK1.6后,對(duì)其做了優(yōu)化,但它畢竟還是涉及到鎖的開(kāi)銷(xiāo)。
每個(gè)線程調(diào)用getInstance方法時(shí)候,都會(huì)涉及到鎖,所以又對(duì)此進(jìn)行了優(yōu)化成為了大家耳熟能詳?shù)碾p重檢查鎖。
雙重檢查鎖
代碼實(shí)現(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)實(shí)if判斷、獲取類(lèi)對(duì)象鎖、if判斷。
上面這段代碼,看似沒(méi)問(wèn)題,其實(shí)還是有問(wèn)題的,比如:指令重排序(需要有JVM知識(shí)墊底哈)
指令重排是什么意思呢?
比如java中簡(jiǎn)單的一句
- lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
會(huì)被編譯器編譯成如下JVM指令:
memory =allocate(); //1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory); //2:初始化對(duì)象
instance =memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
但是這些指令順序并非一成不變,有可能會(huì)經(jīng)過(guò)JVM和CPU的優(yōu)化,指令重排成下面的順序:
memory =allocate(); //1:分配對(duì)象的內(nèi)存空間
instance =memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
ctorInstance(memory); //2:初始化對(duì)象
為了防止指令重排序,所以,我們可以使用volatile來(lái)做文章(注意:volatile能防止指令重排序和線程可見(jiàn)性)。
于是,更好的版本就出來(lái)了。
- 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;
- }
- }
盡管相比前面的版本,確實(shí)改進(jìn)了很多,但依然有同步鎖,還是會(huì)影響性能問(wèn)題。于是,又進(jìn)行優(yōu)化為靜態(tài)內(nèi)部類(lèi)方式:
靜態(tài)內(nèi)部類(lèi)
下面是靜態(tài)內(nèi)部類(lèi)的代碼實(shí)現(xiàn):
利用了內(nèi)部類(lèi)的特性,在JVM底層,能完美的規(guī)避了線程安全的問(wèn)題,這種方式也是目前很多項(xiàng)目里喜歡使用的方式。
但是,還是會(huì)存在潛在的風(fēng)險(xiǎn),什么風(fēng)險(xiǎn)呢?
可以使用 反射 暴力的串改,同樣也會(huì)出現(xiàn)創(chuàng)建多個(gè)實(shí)例:
反射代碼實(shí)現(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)行訪問(wèn)
- constructor.setAccessible(true);
- Object object = constructor.newInstance();
- Object object1 = LazyStaticSingleton.getInstance();
- System.out.println(object == object1);
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- }
- }
這段代碼運(yùn)行結(jié)果為false。
所以,上面說(shuō)的雙重檢查鎖的方式,通過(guò)反射,還是會(huì)存在潛在的風(fēng)險(xiǎn)。怎么辦呢?
在《Effect java 》這本書(shū)中,作者推薦使用枚舉來(lái)實(shí)現(xiàn)單例模式,因?yàn)槊杜e不能被反射。
枚舉
下面是枚舉式的單例模式的代碼實(shí)現(xiàn):
- public enum EnumSingleton {
- INSTANCE;
- private Object data;
- public Object getData() {
- return data;
- }
- public static EnumSingleton getInstance(){
- return INSTANCE;
- }
- }
我們把上面反射的那個(gè)代碼,來(lái)測(cè)試這個(gè)枚舉式單例模式。
- public class EnumTest {
- public static void main(String[] args) {
- try {
- Class<?> clazz = EnumSingleton.class;
- Constructor constructor = clazz.getDeclaredConstructor(null);
- //強(qiáng)行訪問(wèn)
- constructor.setAccessible(true);
- Object object = constructor.newInstance();
- Object object1 = EnumSingleton.getInstance();
- System.out.println(object == object1);
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- }
- }
運(yùn)行這段代碼:
- java.lang.NoSuchMethodException: com.tian.my_code.test.designpattern.singleton.EnumSingleton.<init>()
- 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)
還真的不能用反射來(lái)搞。如果此時(shí)面試官,為什么枚舉不能被反射呢?
為什么枚舉不能被反射呢?
我們?cè)诜瓷涞拇a中
- Constructor constructor = clazz.getDeclaredConstructor(null);
這行代碼是獲取他的無(wú)參構(gòu)造方法。并且,從錯(cuò)誤日志中,我們也可以看到,錯(cuò)誤出現(xiàn)就是在getConstructor0方法中,并且,提示的是沒(méi)有找到無(wú)參構(gòu)造方法。
很奇怪,枚舉也是類(lèi),不是說(shuō)如果我們不給類(lèi)顯示定義構(gòu)造方法時(shí)候,會(huì)默認(rèn)給我們創(chuàng)建一個(gè)無(wú)參構(gòu)造方法嗎?
于是,我想到了一個(gè)辦法,我們可以使用jad這個(gè)工具去反編譯的我們的枚舉式單例的.class文件。
找到我們的class文件所在目錄,然后我們可以執(zhí)行下面這個(gè)命令:
- 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文件所在的目錄。
然后打開(kāi)EnumSingleton.jad 文件:
于是,我就想到了,那我們使用有參構(gòu)造方法來(lái)創(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)行訪問(wèn)
- constructor.setAccessible(true);
- Object object = constructor.newInstance("田維常",996);
- Object object1 = EnumSingleton.getInstance();
- System.out.println(object == object1);
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- }
- }
再次運(yùn)行這段代碼,結(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)建枚舉對(duì)象。
- 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就是用來(lái)判斷是否為枚舉的
- 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;
- }
所以,到此,我們才算真正的理清楚了,為什么枚舉不讓反射的原因。
序列化破壞
我們以非線程安全的餓漢式來(lái)演示一下,看看序列化是如何破壞到了模式的。
- public class ReflectTest {
- public static void main(String[] args) {
- // 準(zhǔn)備兩個(gè)對(duì)象,singleton1接收從輸入流中反序列化的實(shí)例
- 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();
- }
- }
- }
運(yùn)行結(jié)果:
- com.tian.my_code.test.designpattern.singleton.HungrySingleton@7e6cbb7a
- com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41
- false
看到了嗎?
使用序列化是可以破壞到了模式的,這種方式,可能很多人不是很清楚。
如何防止呢?
我們對(duì)非線程安全的餓漢式代碼進(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;
- }
- }
再次運(yùn)行上那段序列化測(cè)試的代碼,其結(jié)果如下:
- com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41
- com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41
- true
嘿嘿,這樣我們是不是就避免了只創(chuàng)建了一個(gè)實(shí)例?
答案:否
在類(lèi)ObjectInputStream的readObject()方法中調(diào)用了另外一個(gè)方法readObject0(false)方法。在readObject0(false)方法中調(diào)用了checkResolve(readOrdinaryObject(unshared))方法。
在readOrdinaryObject方法中有這么一段代碼:
- Object obj;
- try {
- //是否有構(gòu)造方法,有構(gòu)造放就創(chuàng)建實(shí)例
- obj = desc.isInstantiable() ? desc.newInstance() : null;
- } catch (Exception ex) {
- ...
- }
- //判斷單例類(lèi)是否有readResolve方法
- if (desc.hasReadResolveMethod()) {
- Object rep = desc.invokeReadResolve(obj);
- }
- //invokeReadResolve方法中
- if (readResolveMethod != null) {
- //調(diào)用了我們單例類(lèi)中的readResolve,并返回該方法返回的對(duì)象
- //注意:是無(wú)參方法
- return readResolveMethod.invoke(obj, (Object[]) null);
- }
繞了半天,原來(lái)他是這么玩的,上來(lái)就先創(chuàng)建一個(gè)實(shí)例,然后再去檢查我們的單例類(lèi)是否有readResolve無(wú)參方法,我們單例類(lèi)中的readResolve方法
- private Object readResolve(){
- return INSTANCE;
- }
結(jié)論
我們重寫(xiě)了readResolve()無(wú)參方法,表面上看是只創(chuàng)建了一個(gè)實(shí)例,其實(shí)只創(chuàng)建了兩個(gè)實(shí)例。
緊接著,面試官繼續(xù)問(wèn):枚舉式單例能不能被序列化破壞呢?
枚舉式單例能不能被序列化破壞呢?
答案:不能被破壞,請(qǐng)看我慢慢給你道來(lái)。
don't talk ,show me the code。
我們先來(lái)驗(yàn)證一下是否真的不能被破壞,請(qǐng)看代碼:
- public class EnumTest {
- public static void main(String[] args) {
- // 準(zhǔn)備兩個(gè)對(duì)象,singleton1接收從輸入流中反序列化的實(shí)例
- 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();
- }
- }
- }
運(yùn)行結(jié)果:
- INSTANCE
- INSTANCE
- true
確實(shí),枚舉式單例是不會(huì)被序列化所破壞,那為什么呢?總得有個(gè)證件理由吧。
在類(lèi)ObjectInputStream的readObject()方法中調(diào)用了另外一個(gè)方法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")
- //重點(diǎn)
- Enum<?> en = Enum.valueOf((Class)cl, name);
- result = en;
- //...其他代碼省略
- }
- }
- }
- public static <T extends Enum<T>> T valueOf(Class<T> enumType,
- String name) {
- //enumType.enumConstantDirectory()返回的是一個(gè)HashMap
- //通過(guò)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);
- }
- //返回一個(gè)HashMap
- Map<String, T> enumConstantDirectory() {
- if (enumConstantDirectory == null) {
- T[] universe = getEnumConstantsShared();
- if (universe == null)
- throw new IllegalArgumentException(
- getName() + " is not an enum type");
- //使用的是HashMap
- Map<String, T> m = new HashMap<>(2 * universe.length);
- for (T constant : universe)
- m.put(((Enum<?>)constant).name(), constant);
- enumConstantDirectory = m;
- }
- return enumConstantDirectory;
- }
所以,枚舉式單例模式是使用了Map
在Spring中也是有大量使用這種注冊(cè)式單例模式,IOC容器就是典型的代表。
總結(jié)
本文講述了單例模式的定義、單例模式常規(guī)寫(xiě)法。單例模式線程安全問(wèn)題的解決,反射破壞、反序列化破壞等。
注意:不要為了套用設(shè)計(jì)模式,而使用設(shè)計(jì)模式。而是要,在業(yè)務(wù)上遇到問(wèn)題時(shí),很自然地聯(lián)想單設(shè)計(jì)模式作為一種捷徑方法。
單例模式的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
在內(nèi)存中只有一個(gè)實(shí)例,減少內(nèi)存開(kāi)銷(xiāo)??梢员苊鈱?duì)資源的多重占用。設(shè)置全局訪問(wèn)點(diǎn),嚴(yán)格控制訪問(wèn)。
缺點(diǎn)
沒(méi)有借口,擴(kuò)展性很差。如果要擴(kuò)展單例對(duì)象,只有修改代碼,沒(méi)有其他途徑。
單例模式是 不符合開(kāi)閉原則的。
知識(shí)點(diǎn)
單例模式的重點(diǎn)知識(shí)總結(jié):
- 私有化構(gòu)造器
- 保證線程安全
- 延遲加載
- 防止反射攻擊
- 防止序列化和反序列化的破壞
本文轉(zhuǎn)載自微信公眾號(hào)「Java后端技術(shù)全?!梗梢酝ㄟ^(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java后端技術(shù)全棧公眾號(hào)。