单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式属于创建型模式。它提供了一种创建对象的方式,确保只有单个对象被创建,从而节约资源、节约时间。
示例代码 饿汉式单例——在单例类首次加载时就创建实例 简单代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class HungrySingleton { private static final HungrySingleton hungrySingleton = new HungrySingleton(); private HungrySingleton () { } public static HungrySingleton getInstance () { return hungrySingleton; } }
静态代码块实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class HungryStaticSingleton { private static final HungryStaticSingleton hungrySingleton; static { hungrySingleton = new HungryStaticSingleton(); } private HungryStaticSingleton () { } public static HungryStaticSingleton getInstance () { return hungrySingleton; } }
饿汉式单例在类加载的时候就立即初始化,并且创建单例对象。在线程未出现之前就已经实例化了,不可能存在访问安全问题,绝对线程安全。优点:没有加任何锁,执行效率高,比懒汉式单例更好;缺点:类加载的时候就初始化,不管后续用没用上都预占了资源,可能造成内存浪费。
为了解决资源浪费 问题,出现了性能更优的懒汉式单例 。
懒汉式单例——被外部类调用时才创建实例 简单代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class LazySimpleSingleton { private static LazySimpleSingleton lazy = null ; private LazySimpleSingleton () { } public static LazySimpleSingleton getInstance () { if (lazy == null ) { lazy = new LazySimpleSingleton(); } return lazy; } }
懒汉式单例的简单代码实现通过被调用时再实例化 解决了预占资源的问题,但是同时引入了线程安全问题 。
测试线程类ExecutorThread
反射破坏单例 至此,上述单例模式的构造方法中除了加上private外,未做其它处理。如果使用反射来调用其构造方法,然后再调用getInstance()应该会出现两个不同实例,以LazyInnerClassSingleton为例,测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class LazyInnerClassSingletonTest { public static void main (String[] args) { try { Class<?> clazz = LazyInnerClassSingleton.class; Constructor constructor = clazz.getDeclaredConstructor(null ); constructor.setAccessible(true ); Object o1 = constructor.newInstance(); Object o2 = constructor.newInstance(); System.out.println(o1 == o2); } catch (Exception e) { e.printStackTrace(); } } }
运行后可以看到输出结果:false
,很明显创建了两个不同实例。
将构造方法中添加限制,一旦出现重复创建则直接抛出异常,优化实现后如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class LazyInnerClassSingleton { private LazyInnerClassSingleton () { if (LazyHolder.LAZY != null ) { throw new RuntimeException("不允许构建多个实例!" ); } } public static final LazyInnerClassSingleton getInstance () { return LazyHolder.LAZY; } private static class LazyHolder { private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }
再次运行测试类可以看到输出结果:
序列化破坏单例 将一个单例对象创建好后,有时需要将对象序列化写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转为内存对象。反序列化的对象会重新分配内存,即重新创建,如果序列化的目标为单例对象,那就违背了单例模式的初衷,相当于破坏了单例。测试代码如下:
1 2 3 4 5 6 7 8 9 10 public class SeriableSingleton implements Serializable { private static final SeriableSingleton INSTANCE = new SeriableSingleton(); private SeriableSingleton () { } public static SeriableSingleton getInstance () { return INSTANCE; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class SeriableSingletonTest { public static void main (String[] args) { SeriableSingleton s1 = null ; SeriableSingleton s2 = SeriableSingleton.getInstance(); FileOutputStream fos = null ; try { fos = new FileOutputStream("SeriableSingleton.obj" ); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SeriableSingleton.obj" ); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (SeriableSingleton) ois.readObject(); ois.close(); System.out.println(s1); System.out.println(s2); System.out.println(s1 == s2); } catch (Exception e) { e.printStackTrace(); } } }
运行结果:
1 2 3 com.example.designpattern.singleton.hungry.SeriableSingleton@1794d431 com.example.designpattern.singleton.hungry.SeriableSingleton@3e6fa38a false
可以看到,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例设计的初衷。可以通过增加readResolve()来保证序列化的情况下也能够实现单例 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class SeriableSingleton implements Serializable { private static final SeriableSingleton INSTANCE = new SeriableSingleton(); private SeriableSingleton () { } public static SeriableSingleton getInstance () { return INSTANCE; } private Object readResolve () { return INSTANCE; } }
再次运行测试类可以看到:
1 2 3 com.example.designpattern.singleton.hungry.SeriableSingleton@3e6fa38a com.example.designpattern.singleton.hungry.SeriableSingleton@3e6fa38a true
具体原理可查看JDK源码:ObjectInputStream类的readObject()方法。通过分析源码及断点调试可以看到单例类实际上被实例化了两次,只不过新创建的对象没有返回;如果创建对象的频率增加,那就意味着内存开销会增加,为了解决这个问题,出现了注册式单例。
注册式单例——将每一个实例都缓存到统一的容器中,使用唯一标识获取实例 注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:第一种是枚举式登记,第二种是容器缓存。
枚举式单例 测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public enum EnumSingleton { INSTANCE; private Object object; public Object getObject () { return object; } public void setObject (Object object) { this .object = object; } public static final EnumSingleton getInstance () { return INSTANCE; } }
测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class EnumSingletonTest { public static void main (String[] args) { try { EnumSingleton instance1 = null ; EnumSingleton instance2 = EnumSingleton.getInstance(); instance2.setObject(new Object()); FileOutputStream fos = new FileOutputStream("EnumSingleton.obj" ); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.obj" ); ObjectInputStream ois = new ObjectInputStream(fis); instance1 = (EnumSingleton) ois.readObject(); ois.close(); System.out.println(instance1.getObject()); System.out.println(instance2.getObject()); System.out.println(instance1.getObject() == instance2.getObject()); } catch (Exception e) { e.printStackTrace(); } } }
运行结果:
1 2 3 java.lang.Object@548a9f61 java.lang.Object@548a9f61 true
没有做任何特殊处理,可见枚举式单例可以防止序列化破坏单例。原因在于枚举式单例在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例的体现。反编译class文件后可以看到如下内容:
1 2 3 4 5 6 7 static { INSTANCE = new EnumSingleton("INSTANCE" , 0 ); $VALUES = (new EnumSingleton[] { INSTANCE }); }
查看JDK源码可以发现枚举类型是通过类名和Class对象找到唯一的枚举对象,因此,枚举对象不可能被类加载器加载多次。序列化没问题了,那么反射如何?
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class EnumSingletonTest { public static void main (String[] args) { try { Class<?> clazz = EnumSingleton.class; Constructor constructor = clazz.getDeclaredConstructor(String.class, int .class); constructor.setAccessible(true ); EnumSingleton obj = (EnumSingleton) constructor.newInstance("amazing" , int .class); System.out.println(obj); } catch (Exception e) { e.printStackTrace(); } } }
运行结果:
1 2 3 4 java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:484) at com.example.designpattern.singleton.register.EnumSingletonTest.main(EnumSingletonTest.java:47 )
查看JDK源码可以看到在Constructor类的newInstance()方法中做了强制性的判断,如果修饰符是Modifier.ENUM类型会直接抛出异常。枚举式单例是一种比较优雅的写法,在《Effective Java》中也推荐这种写法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public T newInstance (Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, clazz, modifiers); } if ((clazz.getModifiers() & Modifier.ENUM) != 0 ) throw new IllegalArgumentException("Cannot reflectively create enum objects" ); ConstructorAccessor ca = constructorAccessor; if (ca == null ) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
接下来再看容器缓存式单例,测试类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class ContainerSingleton { private ContainerSingleton () { } private static Map<String, Object> ioc = new ConcurrentHashMap<>(); public static Object getBean (String className) { if (!ioc.containsKey(className)) { Object obj = null ; try { obj = Class.forName(className).getDeclaredConstructor().newInstance(); ioc.put(className, obj); } catch (Exception e) { e.printStackTrace(); } return obj; } return ioc.get(className); } }
容器式单例适用于创建实例非常多的情况,便于管理,但它是线程不安全的。
ThreadLocal线程单例——保证线程内部的全局唯一,且天生线程安全 ThreadLocal不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class ThreadLocalSingleton { private ThreadLocalSingleton () { } private static ThreadLocal<ThreadLocalSingleton> threadLocalInstance = ThreadLocal.withInitial(ThreadLocalSingleton::new ); public static ThreadLocalSingleton getInstance () { return threadLocalInstance.get(); } }
ExecutorThread.java
1 2 3 4 5 6 7 public class ExecutorThread implements Runnable { @Override public void run () { ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance(); System.out.println(Thread.currentThread().getName() + ": " + singleton); } }
测试方法类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class ThreadLocalSingletonTest { public static void main (String[] args) { System.out.println(Thread.currentThread()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); Thread t1 = new Thread(new ExecutorThread()); Thread t2 = new Thread(new ExecutorThread()); t1.start(); t2.start(); System.out.println("end" ); } }
运行结果:
1 2 3 4 5 6 7 Thread[main,5 ,main] com.example.designpattern.singleton.threadlocal.ThreadLocalSingleton@1efbd816 com.example.designpattern.singleton.threadlocal.ThreadLocalSingleton@1efbd816 com.example.designpattern.singleton.threadlocal.ThreadLocalSingleton@1efbd816 end Thread-0 : com.example.designpattern.singleton.threadlocal.ThreadLocalSingleton@3ca8ab72 Thread-1 : com.example.designpattern.singleton.threadlocal.ThreadLocalSingleton@107ed120
在主线程中,无论调用多少次,获取到的实例都是同一个,而在两个子线程中分别获取到了不同的实例。前述的单例实现为了达到线程安全的目的,会给方法上锁,以时间换空间;ThreadLocal将所有的对象放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。
Spring实例
ServletContext
ServletConfig
ApplicationContext
DBPool
适用场景
模式优点
在内存中只有一个实例,减少了内存开销
可以避免对资源的多重占用
设置全局访问点,严格控制访问
模式缺点
没有接口,扩展困难
如果要扩展对象,只有修改代码,没有其它途径
总结 使用单例时需要考虑的问题:
程序性能
线程安全
序列化/反序列化破坏单例
反射破坏单例
深度克隆破坏单例
单例模式
多线程
反射
指令重排
反序列化
经典饿汉式
否
能
否
能
经典懒汉式
能
能
能
能
枚举式单例
否
否
否
否
容器式单例
能
否
否
否
ThreadLocal单例
否
能
否
能
综上,平常开发中考虑使用枚举式单例,其它情况根据实际业务场景选择合适的实现方式。
1 2 3 4 5 6 7 8 public enum EnumSingleton { INSTANCE; public static final EnumSingleton getInstance () { return INSTANCE; } }
参考