Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

# 聊聊Java中的单例模式

发表于 2019-12-21 更新于 2019-12-22 分类于 Java ， Design Patterns ， Singleton 阅读次数： 阅读次数： Disqus：

这是个 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 保证了它们不会再被实例化，所以它天生就是单例的。