这是个 Java 面试的高频问题,我也遇到过,以往都是觉得这类题没意思,网上一搜一大堆,也不愿意记,其实说回来,主要还是没
静下心来好好去理解,今天无意中看到一个课程,基本帮我把一些疑惑的点讲清楚了,首先单例是啥意思,这个其实是有范围一说,比如
我起了个 Spring Boot
应用,在这个应用范围内,我的常规 bean 是单例的,意味着 getBean 的时候其实永远只会拿到那一个
对象,那要怎么来写一个单例呢,首先就是传说中的饿汉模式,也是最简单的
public class Singleton1 { // 首先,将构造方法变成私有的 private Singleton1() {}; // 创建私有静态实例,这样第一次使用的时候就会进行创建 private static Singleton instance = new Singleton1(); // 使用这个对象都是通过这个 getInstance 来获取 public static Singleton1 getInstance() { return instance; } // 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...), // 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了 public static Date getDate(String mode) {return new Date();} }
上面借鉴了一些代码,其实这是最基本,也不会错的方法,但是正如其中 getDate
方法里说的问题,有时候并没有想那这个对象,但是因为
我调用了这个类的静态方法,导致对象已经生成了,可能这也是饿汉模式名字的来由,不管三七二十一给你生成个单例就完事了,不管有没有用
但是这种个人觉得也没啥大问题,如果是面试的话最好说出来它的缺点
public class Singleton2 { // 首先,也是先堵死 new Singleton() 这条路,将构造方法变成私有 private Singleton2() {} // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的 private static volatile Singleton2 instance = null; private int m = 9; public static Singleton getInstance() { if (instance == null) { // 加锁 synchronized (Singleton2.class) { // 这一次判断也是必须的,不然会有并发问题 if (instance == null) { instance = new Singleton2(); } } } return instance; } }
这里容易错的有三点,理解了其实就比较好记了
第一点,为啥不在 getInstance 上整个代码块加 synchronized
,这个其实比较容易理解,就是锁的力度太大,性能太差了,这点其实也要去理解,可以举个夸张的例子,比如我一个电商的服务,如果为了避免一个人的订单出现问题,是不是可以从请求入口就把他锁住,到请求结束释放,那么里面做的事情都有保障,然而这显然不可能,因为我们想要这种竞态条件抢占资源的时间尽量减少,防止其他线程等待。
第二点,为啥 synchronized
之已经检查了 instance == null
,还要在里面再检查一次,这个有个术语,叫 double check lock
,但是为啥要这么做呢,其实很简单,想象当有两个线程,都过了第一步为空判断,这个时候只有一个线程能拿到这个锁,另一个线程就等待了,如果不再判断一次,那么第一个线程新建完对象释放锁之后,第二个线程又能拿到锁,再去创建一个对象。
第三点,为啥要 volatile
关键字,原先对它的理解是它修饰的变量在 JMM 中能及时将变量值写到主存中,但是它还有个很重要的作用,就是防止指令重排序, instance = new Singleton();
这行代码其实在底层是分成三条指令执行的,第一条是在堆上申请了一块内存放这个对象,但是对象的字段啥的都还是默认值,第二条是设置对象的值,比如上面的 m 是 9,然后第三条是将这个对象和虚拟机栈上的指针建立引用关联,那么如果我不用 volatile
关键字,这三条指令就有可能出现重排,比如变成了 1-3-2 这种顺序,当执行完第二步时,有个线程来访问这个对象了,先判断是不是空,发现不是空的,就拿去直接用了,是不是就出现问题了,所以这个 volatile
也是不可缺少的
public class Singleton3 { private Singleton3() {} // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性 private static class Holder { private static Singleton3 instance = new Singleton3(); } public static Singleton3 getInstance() { return Holder.instance; } }
这个我个人感觉是饿汉模式的升级版,可以在调用 getInstance
的时候去实例化对象,也是比较推荐的
public enum Singleton { INSTANCE; public void doSomething(){ //todo doSomething } }
枚举很特殊,它在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。