单例模式那些事儿

单例模式是常见的设计模式之一,虽说现在开发框架都帮我们实现了单例模式,但是单例模式的思想和原理还是要去了解的(面试太常问了),今天让我们一起来搞透单例模式。

一. 什么是单例模式?

  • 单例模式就是在内存中只创建一次对象的模式,如果我们在应用中多次使用同一个对象且作用相同时,就应该考虑用单例模式,因为多次创建对象肯定造成内存资源浪费和吞吐量下降,单例模式可以防止空间和创建时造成的时间浪费。

二. 单例模式的几种实现

  • 首先需要明确一点,单例实现的精髓就是私有化构造方法

懒汉式:

class Single{
  private static Single object;
   // 私有化构造方法
  private Single(){};

  public static Single getSingle() {
  // bean为null才去实例化,否则直接返回
    if(object == null) {
    object = new Single();
    }
    return object;
  }
}
 
  • 这种方式是利用一个懒加载的思想,需要Single的时候才会去实例化Single,优点是节省空间,但是在多线程下getSingle方法并不是线程安全的,这个后面再做分析。

饿汉式:

class Single{
  // 静态变量赋值
  private static Single object = new Single();
  // 私有化构造方法
  private Single(){};
  // 获取bean方法直接返回变量
  public static Single getSingle() {
    return object;
  }
}

  • 这种方式是通过给静态变量赋值的方式让Single类编译时被加载,getSingle方法内只有一行,所以是线程安全的,但是因为编译时就加载Single进内存(也可以理解为程序启动时就加载对象进内存),资源浪费也是饿汉式的一个缺点。

下面我们来解析下懒汉式为什么会有线程安全的问题:

                                                                      图1

  • 如图1,多线程情况下,在时刻T,线程A和线程B都判断single为null,从而进入if代码块中都执行了new Single()的操作创建了两个对象,就和我们当初的单例初衷相悖而行。

如何解决线程安全问题呢?下面我们来逐步分析。

在getSingle()方法上加synchronized?

  • 这个方式固然可以解决上面出现的线程安全问题,但是有两个问题:一是每次获取Single对象都要去获取锁,本身申请锁就是一个耗时的行为;二是锁的粒度是整个方法,一般我们在开发中都不会去对整个方法进行加锁,会去尽量减小锁的粒度从而提升代码的性能

下面我们就从这两个问题去优化懒汉式的getSingle()方法:

public static Single getSingle() {
  // 第一次检查防止每次获取bean都加锁
  if(object == null) {
    synchronized(Single.class) {
      // 第二次检查防止第一次创建bean时并发线程多次创建对象
      if(object == null) {
        object = new Single();
      }
    }
  }
  return object;
}
 
  • 我们将同步方法改成的同步代码块,并缩小了锁的粒度,在获取锁之前我们先做一次bean的检查,如果不为空直接返回bean,为空就去争抢锁,抢到锁的线程走到同步代码块中再检查一次bean是否为空(这一次检查是为了防止代码块外部阻塞的线程进来再次创建bean),发现为空就去创建对象,释放锁并返回对象,这个时候在同步代码块外阻塞的线程争抢到锁代码块内部,第二次检查发现bean不为空就直接返回。

这样既保证了线程安全又保证了性能。

但是是不是现在这种方式就真的没有问题了呢?

创建一个对象,在JVM中会经过三步:

    (1)为object对象分配内存空间

    (2)初始化object对象

    (3)将object指向分配好的内存空间

在JVM中是会发生指令重排优化的。

  • 指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。

  • 打个比方:我是一名学生,我今晚回家的任务是写作业和复习白天的功课,然后我发现作业很难有很多不会的,我就先去复习了一下功课然后再去写作业,这就类似JVM指令重排。

那指令重排会造成什么问题?

  • 在多线程场景下有可能先执行了(1)、(3)步骤,但是并没有真正初始化对象,此时别的线程去获取bean发现不为空直接返回了,但是真正使用时就会出现空指针异常

那有什么办法可以防止指令重排呢?

  • 使用volatile关键字,volatile关键字有防止指令重排和线程立即可见作用,在java JUC包下的类中非常常见。

那么我们最终版本单例模式就出炉了!

市面上也有个高大尚的名字:双重校验加锁模式

class Single{
  // volatile关键字防止指令重排造成空指针异常
  private static volatile Single object;
  //  私有化构造方法
  private Single(){};
  
  public static Single getSingle() {
    // 第一次检查防止每次获取bean都加锁
    if(object == null) {
      synchronized(Single.class) {
        // 第二次检查防止第一次创建bean时并发线程多次创建对象
        if(object == null) {
          object = new Single();
        }
      }
    }
    return object;
  }
}
 

  • Spring框架管理的bean也是用了双重校验加锁的方式,唯一的区别是bean没有加volatile,原因也很显而易见:因为spring在容器启动前将所有的bean加载好,不存在立刻获取的情况,所以不会出现上述所说的并发空指针问题。

三. 破坏单例的方式以及如何防止破坏单例?

那么我们单例模式写的那么完美是不是真的能保证单例呢?有没有什么手段可以破坏上面写的单例模式呢?

破环单例的几种方式:

首先从创建对象的几种方式去分析:new,clone,反射,反序列化。

    1. new肯定不行,构造器已经私有化。

    2. clone也不行,没有实现cloneable接口的话也不行。

    3. 反射:暴力反射获取构造器newInstance,实例化新的数据。

防止暴力获取:在私有无参构造里判断下实例是否为空,不为空直接抛出异常阻止构造器初始化对象。

private Singleton(){

   if (singleton != null) {
       throw new RuntimeException();
   }
}

    4. 反序列化也可以。

// 序列化反、序列化代码示例
// 序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("single"));
objectOutputStream.writeObject(singleton);

// 反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("single")));
Singleton newSingleton = (Singleton) objectInputStream.readObject();
 

我们可以看一下源码readObject()做了哪些事情:

public final Object readObject()
        throws IOException, ClassNotFoundException
{
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            // 这步做了反序列化的操作
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

我们进readObject0方法看下,发现代码走到TC_OBJECT的case里

case TC_OBJECT:
   return checkResolve(readOrdinaryObject(unshared));

继续点进readOrdinaryObject方法

// 删去部分源码
private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        // 这里去判断bean里是否有readResolve这个方法
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            // 如果有就调用readResolve() 并将返回值赋给反序列化的bean
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

  • 第八行源码我们发现反序列化在进行反序列化之前会走到hasReadResolveMethod方法里判断当前类是否含有readResolve方法,有的话直接将变量返回,这样我们就能防止反序列化破坏单例了(readResolve这个方法在很多工具类和日期类中都有,就是防止反序列化破坏单例bean)。

由此我们做一个操作:

我们在需要实现单例的bean里加上readResolve方法

private Object readResolve() {
  return singleton;
}
 

总结一下:

  •  1. 防止反序列化破坏单例:进行反序列化的底层方法是readObject0(),里面进行反序列化之前会进行判断,判断单例类里是否有Object readResolve() 方法,如果有将方法内的返回值返回,我们"重写"(不算真正意义的重写)readResolve方法,将单例直接返回。

  •  2. 防止暴力反射破坏单例:在私有无参构造里判断下实例是否为空,不为空直接跑出异常阻止构造器初始化对象。

最终我们单例模式的最终版本出来了:

public class Singleton implements Serializable {

    private static volatile Singleton singleton;

    private Singleton(){
        if (singleton != null) {
            throw new RuntimeException();
        }
    }

    public static Singleton getSingleton() {

        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    private Object readResolve() {

        return singleton;
    }
}
 

到这里你会不会觉得实现单例bean有点麻烦?

《effective java》的作者在书中说“单元素的枚举类型已经成为实现Singleton的最佳方法”;

枚举天然单例和线程安全,暴力反射和反序列化也会报错,实现方式也很简单:

public enum SingleEnum {

   SINGLE_ENUM;

   public SingleEnum getSingleEnum() {

       return SINGLE_ENUM;
   }
}

就和正常的bean一样去定义区别是类型为enum。

到这里,我们今天单例模式的探讨就要结束了,你实现单例会去使用枚举吗?

大家可以关注下我的公众号“阿东编程之路”,里面全是干货!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值