单例模式

概念

单例模式 - 图1

饿汉式

饿汉式单例的标准代码

  1. public class HungrySingleton {
  2. private static final HungrySingleton hungrySingleton = new HungrySingleton();
  3. private HungrySingleton(){}
  4. public static HungrySingleton getInstance(){
  5. return hungrySingleton;
  6. }
  7. }

另一种写法,利用静态代码块机制

  1. public class HungryStaticSingleton {
  2. //先静态,后动态
  3. //先属性,后方法
  4. //先上,后下
  5. private static final HungryStaticSingleton hungrySingleton;
  6. static {
  7. hungrySingleton = new HungryStaticSingleton();
  8. }
  9. private HungryStaticSingleton(){}
  10. public static HungryStaticSingleton getInstance(){
  11. return hungrySingleton;
  12. }
  13. }

懒汉式

单例模式 - 图2

无锁单例 V1.0

  1. public class LazySimpleSingletion {
  2. private static LazySimpleSingletion instance;
  3. private LazySimpleSingletion() {
  4. }
  5. public static LazySimpleSingletion getInstance() {
  6. if (instance == null) {
  7. instance = new LazySimpleSingletion();
  8. }
  9. return instance;
  10. }
  11. }

虽然这解决了饿汉式单例可能带来的内存浪费问题,但在多线程环境下又会出现线程安全问题。

下面模拟多线程问题,编写线程类 ExectorThread

  1. public class ExectorThread implements Runnable{
  2. public void run() {
  3. LazySimpleSingletion instance = LazySimpleSingletion.getInstance();
  4. System.out.println(Thread.currentThread().getName() + ":" + instance);
  5. }
  6. }

客户端测试代码

  1. public class LazySimpleSingletonTest {
  2. public static void main(String[] args) {
  3. Thread t1 = new Thread(new ExectorThread());
  4. Thread t2 = new Thread(new ExectorThread());
  5. t1.start();
  6. t2.start();
  7. System.out.println("End");
  8. }
  9. }

运行结果如下

  1. End
  2. Thread-1:com.yuzhe.design.pattern.singleton.lazy.LazySimpleSingletion@692db0ef
  3. Thread-0:com.yuzhe.design.pattern.singleton.lazy.LazySimpleSingletion@3d41dfc5

由运行结果可知,上面的单例存在线程安全隐患。

运行结果分 2 种情况

  1. 两个线程返回相同的对象
    针对这种情况,又分为两种情况:

    1. 两个线程只创建了一个对象,其中一个线程执行 if (instance == null) 判断时,另一个线程已经执行了 instance = new LazySimpleSingletion(); ,并实例化了单例对象
    2. 两个线程分别创建了单例对象,但是其中一个线程在执行 System.out.println(Thread.currentThread().getName() + ":" + instance); ,打印进行时,另一个线程已经覆盖了前一个线程实例化的对象
  2. 两个线程返回不同的对象
    这种情况下,明显出现了线程安全问题,两个线程分别创建了单例对象,其中一个线程在另一个线程覆盖单例对象前(执行 instance = new LazySimpleSingletion()),打印了线程(执行 System.out.println(Thread.currentThread().getName() + ":" + instance);

一重检查锁 V2.0

为解决 V1.0 的线程安全问题,给 getInstance() 方法加上 synchronized 关键字,使该方法变成线程同步方法

  1. public class LazySimpleSingletion {
  2. private static LazySimpleSingletion instance;
  3. private LazySimpleSingletion() {
  4. }
  5. public synchronized static LazySimpleSingletion getInstance() {
  6. if (instance == null) {
  7. instance = new LazySimpleSingletion();
  8. }
  9. return instance;
  10. }
  11. }

加上 synchronized 关键字之后,当执行其中一个线程调用 getInstance() 方法时,另一个线程再调用 getInstance() 方法,线程状态由 RUNNING 变成了 MONITOR,出现阻塞,直到第一个线程执行完,第二个线程才回复 RUNNING 状态继续调用 getInstance() 方法,如下图所示(这里涉及到 Idea 的多线程调试技巧)

单例模式 - 图3

上图展示了 synchronized 监视锁的运行状态,线程安全问题解决了。但是用 synchronized 加锁时,在线程数量比较多的情况下,如果CPU分配压力上升,会导致大批线程阻塞,从而导致程序性能大幅下降。下面介绍一种兼顾线程安全和程序性能的方式

双重检查锁 V3.0

  1. public class LazyDoubleCheckSingleton {
  2. // volatile 关键字解决指令重排序问题
  3. private volatile static LazyDoubleCheckSingleton instance;
  4. private LazyDoubleCheckSingleton(){}
  5. public static LazyDoubleCheckSingleton getInstance(){
  6. // 检查是否要阻塞
  7. if (instance == null) {
  8. synchronized (LazyDoubleCheckSingleton.class) {
  9. //检查是否要重新创建实例
  10. if (instance == null) {
  11. instance = new LazyDoubleCheckSingleton();
  12. //指令重排序的问题
  13. }
  14. }
  15. }
  16. return instance;
  17. }
  18. }

使用双重检查锁,当第一个线程调用 getInstance() 方法时,第二个线程也可以调用。当第一个线程执行到 synchronized 时会上锁,第二个线程会变成 MONITOR 状态,出现阻塞。此时的阻塞不是基于整个 LazyDoubleCheckSingleton 类的阻塞,而是 getInstance() 方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感知不到。

静态内部类 V 4.0

但使用 synchronized 上锁总归会消耗性能,下面从类初始化的角度考虑,采用静态内部类的方式实现单例

  1. public class LazyStaticInnerClassSingleton {
  2. private LazyStaticInnerClassSingleton(){
  3. }
  4. private static LazyStaticInnerClassSingleton getInstance(){
  5. return LazyHolder.INSTANCE;
  6. }
  7. // 默认不加载
  8. private static class LazyHolder{
  9. private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
  10. }
  11. }

这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronized 的性能问题。内部类是在 getInstance() 方法调用之前初始化,巧妙的避免了线程安全问题。

静态内部类与外部类的加载是相互独立。静态内部类默认不会自动初始化,只有调用静态内部类的方法,静态域,或者构造方法的时候才会加载静态内部类。

单例破坏

反射破坏单例

上面介绍的单例模式的构造方法除了用 private 关键字修饰外,没有任何处理。如果使用反射获得其构造方法,再调用 newInstance() 方法,可以生成两个不同的实例。上代码:

  1. public class ReflectTest {
  2. public static void main(String[] args) {
  3. try {
  4. Class<?> clazz = LazyStaticInnerClassSingleton.class;
  5. // 通过反射获取私有构造方法
  6. Constructor c = clazz.getDeclaredConstructor(null);
  7. // 强制访问
  8. c.setAccessible(true);
  9. // 调用两次构造方法,相当于 new 了2次
  10. Object instance1 = c.newInstance();
  11. Object instance2 = c.newInstance();
  12. System.out.println(instance1);
  13. System.out.println(instance2);
  14. System.out.println(instance1 == instance2);
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }

运行结果如下

  1. com.yuzhe.design.pattern.singleton.lazy.LazyStaticInnerClassSingleton@6d6f6e28
  2. com.yuzhe.design.pattern.singleton.lazy.LazyStaticInnerClassSingleton@135fbaa4
  3. false

显然创建了2个不同的实例。如何避免这个问题?现在做如下优化,在其构造方法种做一些限制,如果单例实例已经存在,则直接抛出异常。

  1. public class LazyStaticInnerClassSingleton {
  2. private LazyStaticInnerClassSingleton() {
  3. if (LazyHolder.INSTANCE != null) {
  4. throw new RuntimeException("不允许非法访问");
  5. }
  6. }
  7. public static LazyStaticInnerClassSingleton getInstance() {
  8. return LazyHolder.INSTANCE;
  9. }
  10. private static class LazyHolder {
  11. private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
  12. }
  13. }

在运行测试,得到如下结果

  1. java.lang.reflect.InvocationTargetException
  2. at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  3. at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
  4. at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
  5. at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
  6. at com.yuzhe.design.pattern.singleton.test.ReflectTest.main(ReflectTest.java:19)
  7. Caused by: java.lang.RuntimeException: 不允许非法访问
  8. at com.yuzhe.design.pattern.singleton.lazy.LazyStaticInnerClassSingleton.<init>(LazyStaticInnerClassSingleton.java:17)
  9. ... 5 more

至此,自认为是史上最牛的单例模式的实现方式大功告成。但看似完美的单例写法还是有可能被破坏。

序列化破坏单例

解决方案

  1. private Object readResolve() {
  2. System.out.println("调用了 SeriableSingleton 的 readResolve方法");
  3. return INSTANCE;
  4. }

注册式

枚举式

  1. public enum EnumSingleton {
  2. INSTANCE;
  3. private Object data;
  4. public Object getData() {
  5. return data;
  6. }
  7. public void setData(Object data) {
  8. this.data = data;
  9. }
  10. public static EnumSingleton getInstance(){return INSTANCE;}
  11. }

容器式

  1. public class ContainerSingleton {
  2. private ContainerSingleton(){}
  3. private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
  4. public static Object getInstance(String className){
  5. Object instance = null;
  6. if(!ioc.containsKey(className)){
  7. try {
  8. instance = Class.forName(className).newInstance();
  9. ioc.put(className, instance);
  10. }catch (Exception e){
  11. e.printStackTrace();
  12. }
  13. return instance;
  14. }else{
  15. return ioc.get(className);
  16. }
  17. }
  18. }

线程式

  1. public class ThreadLocalSingleton {
  2. private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance =
  3. new ThreadLocal<ThreadLocalSingleton>(){
  4. @Override
  5. protected ThreadLocalSingleton initialValue() {
  6. return new ThreadLocalSingleton();
  7. }
  8. };
  9. private ThreadLocalSingleton(){}
  10. public static ThreadLocalSingleton getInstance(){
  11. return threadLocaLInstance.get();
  12. }
  13. }