单例模式总结

Posted enjoyjava

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式总结相关的知识,希望对你有一定的参考价值。

在设计单例模式时,需要考虑以下几点:

构造器私有化

保证线程安全

延迟加载

防止序列号和反序列化破坏单例

防止反射攻击破坏单例

1.饿汉式单例模式

优点:天生线程安全,缺点:不管用不用的到,都去实例化

 1 package com.gupaoedu.vip.pattern.singleton.hungry;
 2 
 3 /**
 4  * 饿汉式单例模式三要素
 5  * 1.构造方法私有
 6  * 2.静态的获取单例的方法
 7  * 3.实例在类加载还未被初始化的时候就创建了  private static final
 8  * 写在静态块里面也可以
 9  * 缺点:不管用不用,都实例化,会浪费内存空间===>改进:懒汉式单例
10  */
11 public class HungrySingleton 
12 
13 //    private static final HungrySingleton  hungrySingleton= new HungrySingleton();
14     private static final HungrySingleton hungrySingleton;
15     static 
16         hungrySingleton= new HungrySingleton();
17     
18     private HungrySingleton()
19 
20     public static HungrySingleton getInstance()
21         return hungrySingleton;
22     
23 

2. 懒汉式单例模式:

所谓懒汉式,就是让单例的实例化,推迟到被调用 的时候再去创建,同时还要保证线程安全

 1 package com.gupaoedu.vip.pattern.singleton.lazy;
 2 
 3 /**
 4  * 1.构造方法私有化
 5  * 2.提供全局访问点
 6  */
 7 public class LazySimpleSingleton 
 8     private  static  LazySimpleSingleton lazy;
 9 
10     private LazySimpleSingleton()
11 
12     /*public static LazySimpleSingleton getInstance()
13         if(null == lazy)  //在这里加断点调试
14             lazy = new LazySimpleSingleton();
15         
16         return lazy;
17     */
18     //加synchronized关键字可以解决线程安全问题,但是会带来2个问题,1:性能问题,2 synchronized有可能会锁住整个LazySimpleSingleton类,甚至导致死锁
19     //解决办法:使用双重加锁检查机制
20     public synchronized static LazySimpleSingleton getInstance()
21         if(null == lazy)  //在这里加断点调试
22             lazy = new LazySimpleSingleton();
23         
24         return lazy;
25     
26 
27 

最原始的懒汉式单例,就是注释掉的代码那样, 但是当多个线程同时进入if 判断并new对象这个步骤,此时就已经产生了多个实例了,而使用 synchronized 关键字让getInstance方法变成同步方法,虽然能解决线程安全问题,但是会带来2个其他问题: 性能问题, 以及synchronized可能会锁住整个类,让高并发情况下变成了异步执行,有没有一种折中的方法来实现呢?  这里就可以使用双重加锁检查的单例:

 1 package com.gupaoedu.vip.pattern.singleton.lazy;
 2 
 3 public class LazyDoubleCheckSingleton  
 4     //加上volatile关键字,保证线程可见性
 5     private /*volatile*/ static  LazyDoubleCheckSingleton lazy;
 6 
 7     private LazyDoubleCheckSingleton()
 8 
 9 
10     //加synchronized关键字可以解决线程安全问题,但是会带来2个问题,1:性能问题,2 synchronized有可能会锁住整个LazySimpleSingleton类,甚至导致死锁
11 
12     //解决办法:使用双重加锁检查机制
13     /*public synchronized static LazyDoubleCheckSingleton getInstance()
14         if(null == lazy)  //在这里加断点调试
15             lazy = new LazyDoubleCheckSingleton();
16         
17         return lazy;
18     */
19 
20     //synchronized是一种类锁,在这里把加锁放到if里面去
21     public  static LazyDoubleCheckSingleton getInstance()
22         if(null == lazy)  //在这里加断点调试
23             synchronized(LazyDoubleCheckSingleton.class)
24                 if(null == lazy)  //这里如果不加判断,第二个线程还是会有可能再new一次对象
25                     lazy = new LazyDoubleCheckSingleton();
26                 
27             
28         
29         return lazy;
30     
31 
32 

内层的if(null == lazy) 判断不能少,当第一个线程获取锁并成功创建实例后,释放锁之后,还未return之前,假如有第二个线程进来了,这时 lazy还是null, 这个线程也能获得锁并创建实例,然后两个线程一起返回,那么这时就

存在的问题: 反射,序列化仍然会破坏单例,解决方法:在构造参数中检查单例如果已经被实例化过了,就抛出一个runtimeException,如果单例实现了序列化接口,需要重写readResolve()方法

3.静态内部类单例

利用内部类的特性,JVM底层 的执行逻辑,避免了线程安全问题, LazyHolder里面的逻辑需要等到外部方法调用时才执行,
 1 package com.gupaoedu.vip.pattern.singleton.lazy;
 2 
 3 public class LazyInnerClassSingleton 
 4     /**
 5      * 即便使用了静态内部类,调用者仍可以通过反射机制以及序列化和反序列化来暴力 破坏单例,这里需要加判断来阻止
 6      */
 7     private LazyInnerClassSingleton()
 8        if (LazyHolder.LAZY != null)
 9             throw  new RuntimeException("不允构建多个实例!");
10         
11 
12     
13 
14     /**
15      * 利用内部类的特性,JVM底层 的执行逻辑,避免了线程安全问题
16      * LazyHolder里面的逻辑需要等到外部方法调用时才执行,
17      * @return
18      */
19     public static final LazyInnerClassSingleton getInstance()
20         return LazyHolder.LAZY;
21     
22 
23     private static  class  LazyHolder
24         private  static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
25     
26 
27 

 4. 注册式单例:

4.1 枚举式单例:    天生不会被反射,序列化破坏的单例模式(双重加锁检查单例,静态内部类单例都可以被反射攻击破坏单例 ,序列化及反序列化破坏单例)

 1 package com.gupaoedu.vip.pattern.singleton.register;
 2 
 3 /**
 4  * 注册式单例方式1:枚举式单例
 5  */
 6 public enum EnumSingleton 
 7     INSTANCE;
 8     private  Object data;
 9 
10     public Object getData() 
11         return data;
12     
13 
14   /*  public void setData(Object data) 
15         this.data = data;
16     */
17 
18     public static  EnumSingleton getInstance()
19         return INSTANCE;
20     
21 
22 

4.2 注册式单例:

 1 package com.gupaoedu.vip.pattern.singleton.register;
 2 
 3 import java.util.Map;
 4 import java.util.concurrent.ConcurrentHashMap;
 5 
 6 /**
 7  * 注册式单例
 8  * 优点:对象方便管理,属于懒加载
 9  * 缺点:getbean线程非安全
10  */
11 public class ContainerSingleton 
12 //ConcurrentHashMap只保证put的时候是线程安全的,但不能保证get的时候线程安全
13     private static Map<String,Object> ioc = new ConcurrentHashMap<>();
14 
15     private static Object lock = new Object();
16     private  ContainerSingleton()
17 
18     /**
19      * 这里i获取实例的时候会存在线程安全问题
20      * @param className
21      * @return
22      */
23     public static Object getBean(String className)
24        synchronized (lock)
25            //不存在,先赋值
26            if(!ioc.containsKey(className))
27                Object obj = null;
28                try
29                    obj = Class.forName(className).newInstance();
30                    ioc.put(className,obj);
31                 catch (Exception e)
32                    e.printStackTrace();
33                
34                return  obj;
35            
36            //存在,直接返回
37            return ioc.get(className);
38        
39     
40 
41 

4.3 为什么枚举式单例不能通过反射和序列化方式破坏单例?先来测试一下

4.3.1 测试反射破坏枚举式单例:

 1 package com.gupaoedu.vip.pattern.singleton.test;
 2 
 3 import com.gupaoedu.vip.pattern.singleton.register.EnumSingleton;
 4 
 5 import java.lang.reflect.Constructor;
 6 
 7 public class EnumSingletonReflectTest 
 8     public static void main(String[] args) 
 9         try 
10            Class clazz = EnumSingleton.class;
11 //            Constructor c = clazz.getDeclaredConstructor();
12 //            c.setAccessible(true);
13 //            c.newInstance();
14 
15             /**
16              * 执行 c.newInstance(); 报错,因为压根就没有一个无参构造了
17              * java.lang.NoSuchMethodException: com.gupaoedu.vip.pattern.singleton.register.EnumSingleton.<init>()
18              *     at java.lang.Class.getConstructor0(Class.java:3082)
19              *     at java.lang.Class.getDeclaredConstructor(Class.java:2178)
20              *     at com.gupaoedu.vip.pattern.singleton.test.EnumSingletonReflectTest.main(EnumSingletonReflectTest.java:11)
21              */
22 
23 //获取有参构造,继续通过反射破坏单例
24             Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
25             EnumSingleton obj = (EnumSingleton)c.newInstance("tom", 999);
26             System.out.println(obj);
27             /**
28              * 这时报错如下:
29              * java.lang.IllegalAccessException: Class com.gupaoedu.vip.pattern.singleton.test.EnumSingletonReflectTest can not access a member of class com.gupaoedu.vip.pattern.singleton.register.EnumSingleton with modifiers "private"
30              *     at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
31              *     at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
32              *     at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
33              *     at java.lang.reflect.Constructor.newInstance(Constructor.java:413)
34              *     at com.gupaoedu.vip.pattern.singleton.test.EnumSingletonReflectTest.main(EnumSingletonReflectTest.java:25)
35              */
36          catch (Exception e)
37             e.printStackTrace();
38         
39 
40     
41 

原理分析如下:

反编译EnumSingleton.class ,得到如下文件:

 1 // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
 2 // Jad home page: http://www.kpdus.com/jad.html
 3 // Decompiler options: packimports(3) 
 4 // Source File Name:   EnumSingleton.java
 5 
 6 package com.gupaoedu.vip.pattern.singleton.register;
 7 
 8 
 9 public final class EnumSingleton extends Enum
10 
11 
12     public static EnumSingleton[] values()
13     
14         return (EnumSingleton[])$VALUES.clone();
15     
16 
17     public static EnumSingleton valueOf(String name)
18     
19         return (EnumSingleton)Enum.valueOf(com/gupaoedu/vip/pattern/singleton/register/EnumSingleton, name);
20     
21 
22     private EnumSingleton(String s, int i)
23     
24         super(s, i);
25     
26 
27     public Object getData()
28     
29         return data;
30     
31 
32     public static EnumSingleton getInstance()
33     
34         return INSTANCE;
35     
36 
37     public static final EnumSingleton INSTANCE;
38     private Object data;
39     private static final EnumSingleton $VALUES[];
40 
41     static 
42     
43         INSTANCE = new EnumSingleton("INSTANCE", 0);
44         $VALUES = (new EnumSingleton[] 
45             INSTANCE
46         );
47     
48 

可以看到,枚举式单例本质上是使用了饿汉式单例,从根本上保证了线程安全性,并且内部没有无参构造,只有一个2个入参的构造方法,从   EnumSingleton obj = (EnumSingleton)c.newInstance("tom", 999);

这行代码的newInstance方法入手,查看其源码如下:

技术图片

可以看到,如果类有修饰符并且这个类是枚举类型,那么就抛出一个异常,从JDK层面杜绝了通过反射方式破坏枚举式单例

4.3.2 测试序列化及反序列化破坏枚举式单例:

 1 package com.gupaoedu.vip.pattern.singleton.test;
 2 
 3 import com.gupaoedu.vip.pattern.singleton.register.EnumSingleton;
 4 
 5 import java.io.FileInputStream;
 6 import java.io.FileOutputStream;
 7 import java.io.ObjectInputStream;
 8 import java.io.ObjectOutputStream;
 9 
10 /**
11  * 测试 序列化破坏枚举式单例--
12  */
13 public class EnumSingletonTest 
14     public static void main(String[] args) 
15         EnumSingleton s1 = null;
16         EnumSingleton s2 = EnumSingleton.getInstance();
17         FileOutputStream fos = null;
18         try 
19             fos = new FileOutputStream("EnumSingleton.obj")
20 ;
21             ObjectOutputStream oos = new ObjectOutputStream(fos);
22             oos.writeObject(s2);
23             oos.flush();
24             oos.close();
25             FileInputStream fis = new FileInputStream("EnumSingleton.obj");
26             ObjectInputStream ois = new ObjectInputStream(fis);
27             s1 = (EnumSingleton)ois.readObject();
28             System.out.println(s1);
29             System.out.println(s2);
30             System.out.println(s1 == s2);
31             /**
32              * 输出结果:INSTANCE
33              * INSTANCE
34              * true 说明枚举式单例,能够保证反序列化不破坏单例
35              */
36          catch (Exception e)
37             e.printStackTrace();
38         
39 
40     
41 

 

分析原理如下:从ObjectInputStream类的readObject() 方法入手,

技术图片

readObject0内部:

 1 private Object readObject0(boolean unshared) throws IOException 
 2     boolean oldMode = bin.getBlockDataMode();
 3     if (oldMode) 
 4         int remain = bin.currentBlockRemaining();
 5         if (remain > 0) 
 6             throw new OptionalDataException(remain);
 7          else if (defaultDataEnd) 
 8             /*
 9              * Fix for 4360508: stream is currently at the end of a field
10              * value block written via default serialization; since there
11              * is no terminating TC_ENDBLOCKDATA tag, simulate
12              * end-of-custom-data behavior explicitly.
13              */
14             throw new OptionalDataException(true);
15         
16         bin.setBlockDataMode(false);
17     
18 
19     byte tc;
20     while ((tc = bin.peekByte()) == TC_RESET) 
21         bin.readByte();
22         handleReset();
23     
24 
25     depth++;
26     totalObjectRefs++;
27     try 
28         switch (tc) 
29             case TC_NULL:
30                 return readNull();
31 
32             case TC_REFERENCE:
33                 return readHandle(unshared);
34 
35             case TC_CLASS:
36                 return readClass(unshared);
37 
38             case TC_CLASSDESC:
39             case TC_PROXYCLASSDESC:
40                 return readClassDesc(unshared);
41 
42             case TC_STRING:
43             case TC_LONGSTRING:
44                 return checkResolve(readString(unshared));
45 
46             case TC_ARRAY:
47                 return checkResolve(readArray(unshared));
48 
49             case TC_ENUM:
50                 return checkResolve(readEnum(unshared));
51 
52             case TC_OBJECT:
53                 return checkResolve(readOrdinaryObject(unshared));
54 
55             case TC_EXCEPTION:
56                 IOException ex = readFatalException();
57                 throw new WriteAbortedException("writing aborted", ex);
58 
59             case TC_BLOCKDATA:
60             case TC_BLOCKDATALONG:
61                 if (oldMode) 
62                     bin.setBlockDataMode(true);
63                     bin.peek();             // force header read
64                     throw new OptionalDataException(
65                         bin.currentBlockRemaining());
66                  else 
67                     throw new StreamCorruptedException(
68                         "unexpected block data");
69                 
70 
71             case TC_ENDBLOCKDATA:
72                 if (oldMode) 
73                     throw new OptionalDataException(true);
74                  else 
75                     throw new StreamCorruptedException(
76                         "unexpected end of block data");
77                 
78 
79             default:
80                 throw new StreamCorruptedException(
81                     String.format("invalid type code: %02X", tc));
82         
83      finally 
84         depth--;
85         bin.setBlockDataMode(oldMode);
86     

当反序列化读取到的流里面,发现这是一个对象时(TC_OBJECT),则调用 checkResolve(readOrdinaryObject(unshared))

 

技术图片

 

技术图片

一个类显然是有构造方法的,不管这个构造方法是私有还是公有,都会初始化,那么 desc.newInstance()必定会执行:

技术图片

但是,在初始化之后,还会判断这个类有没有readResolve方法,如果有,就调用该方法, 那么为了解决双重加锁检查和静态 内部类单例不被反序列化破坏单例,可以在单例类里面重写readResolve方法,这个方法里面直接返回我们之前创建好的单实例即可解决单例被反序列化破坏的问题:

技术图片

这样设计是JDK考虑到单例被破坏的情况,是一种亡羊补牢的方式,重写readResolve()方法只不过是覆盖了反序列号出来的对象,实际上还是创建了2次,但是发生在JVM层面,相对来说比较安全,而之前反序列化出来的对象会被GC; 那么对于枚举式单例,在readObject0()方法里面则会走这个 分支:

技术图片技术图片

技术图片

也就是说,枚举式单例,在反序列化时,直接通过一个类名和枚举名字确定了唯一的一个实例,保证单例不被反序列化破坏

单例模式所有代码:https://github.com/liuch0228/GP-pattern

 

以上是关于单例模式总结的主要内容,如果未能解决你的问题,请参考以下文章

历史最全单例模式总结

单例模式总结

java 单例模式总结

设计模式-单例模式(最全总结)

Java 单例模式 总结整理

单例模式总结