单例模式的应用场景 单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 例如,公司 CEO、部门经理在一个公司只能存在一个等。在 J2EE 标准中,ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接池也都是单例形式。单例模式可分为以下几种:
饿汉式单例
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题。
优点: 没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
缺点: 类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。
饿汉式写法简单,试用与单例对象少的情况,Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例。
1 2 3 4 5 6 7 8 9 10 public class HungrySingleton { public static final HungrySingleton INSTANCE = new HungrySingleton(); private HungrySingleton () {} public static HungrySingleton getInstance () { return INSTANCE; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class HungryStaticSingleton { public static final HungryStaticSingleton INSTANCE; static { INSTANCE = new HungryStaticSingleton(); } private HungryStaticSingleton () {} public static HungryStaticSingleton getInstance () { return INSTANCE; } }
懒汉式单例
懒汉式单例在被外部类调用的时候内部类才会加载
缺点:不加synchronized可能会出现线程安全问题,但是加上synchronized关键字以后,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。
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 29 30 public class SimpleLazySingleton { public static SimpleLazySingleton INSTANCE = null ; private SimpleLazySingleton () { } public synchronized static SimpleLazySingleton getInstance () { if (INSTANCE == null ) { INSTANCE = new SimpleLazySingleton(); } return INSTANCE; } }
比上一个方法稍微好点,但是因为加了锁,性能仍有问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class DoubleCheckLazySingleton { public volatile static DoubleCheckLazySingleton INSTANCE = null ; private DoubleCheckLazySingleton () {} public static DoubleCheckLazySingleton getInstance () { if (INSTANCE == null ) { synchronized (DoubleCheckLazySingleton.class) { if (INSTANCE == null ) { INSTANCE = new DoubleCheckLazySingleton(); } } } return INSTANCE; } }
这种形式兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题,
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 public class InnerClassLazySingleton { private InnerClassLazySingleton () {} public static final InnerClassLazySingleton getInstance () { return LazyHolder.INSTANCE; } public static class LazyHolder { public static final InnerClassLazySingleton INSTANCE = new InnerClassLazySingleton(); } }
这种形式兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题。内部类一定是要在方 法调用之前初始化,巧妙地避免了线程安全问题。
单例被破坏 反射破坏单例 上述所有的单例模式的构造方法除了加上 private 以外,没有做任何处 理。如果我们使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会两个不同的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class InnerClassSingletonTest { public static void main (String[] args) { try { Class<?> clazz = InnerClassLazySingleton.class; Constructor<?> constructor = clazz.getDeclaredConstructor(null ); constructor.setAccessible(true ); Object o = constructor.newInstance(); InnerClassLazySingleton o2 = InnerClassLazySingleton.getInstance(); System.out.println(o); System.out.println(o2); System.out.println(o == o2); } catch (Exception e) { e.printStackTrace(); } } }
1 2 3 com.design.pattern.singleton.lazy.InnerClassLazySingleton@776ec8df com.design.pattern.singleton.lazy.InnerClassLazySingleton@4eec7777 false
此时你会发现两次对象并不是同一个,也就是单例被破坏了,此时,为了避免这种情况,我们需要在构造方法中做一下限制,仍以静态内部类演示,代码如下:
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 29 30 31 public class InnerClassLazySingleton { private InnerClassLazySingleton () { if (LazyHolder.INSTANCE != null ) { throw new RuntimeException("不允许重复创建" ); } } public static final InnerClassLazySingleton getInstance () { return LazyHolder.INSTANCE; } public static class LazyHolder { public static final InnerClassLazySingleton INSTANCE = new InnerClassLazySingleton(); } }
再次运行测试类,则出现以下异常:
序列化破坏单例 当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时 再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存, 即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。来看一段演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class SerializableSingleton implements Serializable { public final static SerializableSingleton INSTANCE = new SerializableSingleton(); private SerializableSingleton () {} public static SerializableSingleton 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 public class SerializableSingletonTest { public static void main (String[] args) { SerializableSingleton s1 = null ; SerializableSingleton s2 = SerializableSingleton.getInstance(); FileOutputStream fos = null ; try { fos = new FileOutputStream("SeriableSingleton.txt" ); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SeriableSingleton.txt" ); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (SerializableSingleton) 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.design.pattern.singleton.seriable.SerializableSingleton@306a30c7 com.design.pattern.singleton.seriable.SerializableSingleton@6d311334 false
运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两 次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加**readResolve()**方法即可。修改后的代码
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 SerializableSingleton implements Serializable { public final static SerializableSingleton INSTANCE = new SerializableSingleton(); private SerializableSingleton () {} public static SerializableSingleton getInstance () { return INSTANCE; } private Object readResolve () { return INSTANCE; } }
再次运行测试类,得出如下结果:
1 2 3 com.design.pattern.singleton.seriable.SerializableSingleton@6d311334 com.design.pattern.singleton.seriable.SerializableSingleton@6d311334 true
此时我们发现反序列化后的对象和手动创建的对象是同一个对象(源码分析待补充)。虽然,增加 readResolve()方法返回实例,解决了单 例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,难道真的就没办法从根本上解决问题吗?下面我们来注册式单例也许能帮助到你。
注册式单例
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标 识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public enum EnumSingleton { INSTANCE; private Object data; public Object getData () { return data; } public void setData (Object data) { this .data = data; } public static 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 27 28 29 public class EnumSingletonTest { public static void main (String[] args) { EnumSingleton s1 = null ; EnumSingleton s2 = EnumSingleton.getInstance(); s2.setData(new Object()); FileOutputStream fos = null ; try { fos = new FileOutputStream("EnumSingleton.txt" ); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.txt" ); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (EnumSingleton) ois.readObject(); ois.close(); System.out.println(s1.getData()); System.out.println(s2.getData()); System.out.println(s1.getData() == s2.getData()); } catch (Exception e) { e.printStackTrace(); } } }
1 2 3 java.lang.Object@b81eda8 java.lang.Object@b81eda8 true
通过结果可以发现枚举式单例能完美避开反序列化带来的问题。
我们再用反射来进行测试:
1 2 3 4 5 6 7 8 9 10 11 public class EnumSingletonTest { public static void main (String[] args) { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(); c.newInstance(); } catch (Exception e) { e.printStackTrace(); } } }
则会得到如下异常:
报的是 java.lang.NoSuchMethodException 异常,意思是没找到无参的构造方法。打开JDK中的Enum类,我们只看到了一个这样的构造方法:
1 2 3 4 protected Enum (String name, int ordinal) { this .name = name; this .ordinal = ordinal; }
那我们再来做一个这样的测试:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(String.class, int .class); c.setAccessible(true ); EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("Test" , 1234 ); } catch (Exception e) { e.printStackTrace(); } }
运行结果如下:
这时错误已经非常明显了,告诉我们 Cannot reflectively create enum objects,不能 用反射来创建枚举类型。我们进入Constructor中的neInstance()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @CallerSensitive 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); } } 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; }
在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型, 直接抛出异常。
容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ContainerSingleton { private ContainerSingleton () {} private static Map<String, Object> ioc = new ConcurrentHashMap<>(); public static Object getInstance (String className) { synchronized (ioc) { if (!ioc.containsKey(className)) { Object obj = null ; try { obj = Class.forName(className).newInstance(); ioc.put(className, obj); } catch (Exception e) { e.printStackTrace(); } return obj; } else { return ioc.get(className); } } } }
ThreadLocal 线程单例
ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。
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 public class ThreadLocalSingleton { private ThreadLocalSingleton () { } private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() { @Override protected ThreadLocalSingleton initialValue () { return new ThreadLocalSingleton(); } }; public static ThreadLocalSingleton getInstance () { return threadLocalInstance.get(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ThreadLocalSingletonTest { public static void main (String[] args) { System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println("end" ); new Thread(() -> { System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); System.out.println("end" ); }).start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); System.out.println("end" ); }).start(); } }
1 2 3 4 5 6 7 8 9 10 11 12 com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@24d46ca6 com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@24d46ca6 com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@24d46ca6 end Thread-0:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@4082029 Thread-0:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@4082029 Thread-0:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@4082029 end Thread-1:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@1e542a06 Thread-1:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@1e542a06 Thread-1:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@1e542a06 end
通过运行结果可以发现,在主线程 main中无论调用多少次,获取到的实例都是同一个,都在两个子线程中也都分别获取到了不同的实例。那么 ThreadLocal 是如果实现这样的效果的呢?我们知道上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。ThreadLocal 将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以 空间换时间来实现线程间隔离的。