转载

聊聊Java中的单例模式

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

原文  https://nicksxs.me/2019/12/21/聊聊Java中的单例模式/
正文到此结束
Loading...