面试必问题:单例模式有几种写法?

157 篇文章 1 订阅

 懒汉模式

懒汉模式是变种最多的单例模式。

所谓懒汉即先不初始化单例,等第一次使用的时候再初始化,即“懒加载”。

基础的懒汉

// 饱汉
// UnThreadSafe
public class Singleton1 {
    private static Singleton1 singleton = null;

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        if (singleton == null) {
            singleton = new Singleton1();
        }

        return singleton;
    }
}

懒汉模式的核心就是懒加载。好处是启动速度快、节省资源,一直到实例被第一次访问,才需要初始化单例;小坏处是写起来麻烦,大坏处是线程不安全,if语句存在竞态条件。

懒汉-变种1

最粗暴的犯法是用synchronized关键字修饰getInstance()方法,这样能达到绝对的线程安全。

// 饱汉
// ThreadSafe
public class Singleton1_1 {
    private static Singleton1_1 singleton = null;

    private Singleton1_1() {
    }

    public synchronized static Singleton1_1 getInstance() {
        if (singleton == null) {
            singleton = new Singleton1_1();
        }

        return singleton;
    }
}

变种1的好处是写起来简单,且绝对线程安全;坏处是并发性能极差,事实上完全退化到了串行。单例只需要初始化一次,但就算初始化以后,synchronized的锁也无法避开,从而getInstance()完全变成了串行操作。性能不敏感的场景建议使用。

懒汉-变种2

针对变种1中单例初始化后锁仍然无法避开的问题,变种2在变种1的外层又套了一层check,加上synchronized内层的check,即所谓“双重检查锁”(Double Check Lock,简称DCL)。

// 饱汉
// UnThreadSafe
public class Singleton1_2 {
    private static Singleton1_2 singleton = null;
    public int f1 = 1; // 触发部分初始化问题
    public int f2 = 2;

    private Singleton1_2() {
    }

    public static Singleton1_2 getInstance() {
        // may get half object
        if (singleton == null) {
            synchronized (Singleton1_2.class) {
                if (singleton == null) {
                    singleton = new Singleton1_2();
                }
            }
        }

        return singleton;
    }
}

变种2的核心是DCL,看起来变种2似乎已经达到了理想的效果:懒加载+线程安全。可惜的是,正如注释中所说,DCL仍然是线程不安全的,由于指令重排序,你可能会得到“半个对象”,即”部分初始化“问题。

问题出在这行简单的赋值语句:

singleton = new Singleton1_2();

它并不是一个原子操作。事实上,它可以”抽象“为下面几条JVM指令:

memory = allocate();    //1:分配对象的内存空间
initInstance(memory);    //2:初始化对象(对f1、f2初始化)
instance = memory;        //3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:

memory = allocate();    //1:分配对象的内存空间
instance = memory;        //3:设置instance指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory);    //2:初始化对象

可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向内存memory时,这段崭新的内存还没有初始化——即,引用instance指向了一个”被部分初始化的对象”。此时,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。最终可能导致其他线程执行时引发:对象尚未初始化错误。

解决这个该问题,只需要将instance声明为volatile变量:

private static volatile  Singleton1_2 singleton = null; 

饿汉模式

所谓饿汉即类加载时初始化单例,以后访问时直接返回即可。

// 饿汉
// ThreadSafe
public class Singleton2 {
    private static final Singleton2 singleton = new Singleton2();

    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        return singleton;
    }
}

饿汉的好处是天生的线程安全(得益于类加载机制),写起来超级简单,使用时没有延迟;坏处是有可能造成资源浪费(如果类加载后就一直不使用单例的话)。

值得注意的时,单线程环境下,饿汉与懒汉在性能上没什么差别;但多线程环境下,由于懒汉需要加锁,饿汉的性能反而更优。

Holder模式(静态内部类)

我们既希望利用饿汉模式中静态变量的方便和线程安全;又希望通过懒加载规避资源浪费。Holder模式满足了这两点要求:核心仍然是静态变量,足够方便和线程安全;通过静态的Holder类持有真正实例,间接实现了懒加载。

// Holder模式
// ThreadSafe
public class Singleton3 {
    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        return SingletonHolder.singleton;
    }

    private static class SingletonHolder {
        private static final Singleton3 singleton = new Singleton3();

        private SingletonHolder() {
        }
    }
}

相对于饿汉模式,Holder模式仅增加了一个静态内部类的成本,与懒汉的变种2效果相当(略优),都是比较受欢迎的实现方式。同样建议考虑。

枚举模式

用枚举实现单例模式,相当好用,但可读性是不存在的。

将枚举的静态成员变量作为单例的实例:

// 枚举
// ThreadSafe
public enum Singleton4 {
    SINGLETON;

    public void sayHello() {

    }
}

代码量比饿汉模式更少。但用户只能直接访问实例Singleton4.SINGLETON——事实上,这样的访问方式作为单例使用也是恰当的,只是牺牲了静态工厂方法的优点,如无法实现懒加载。

通过反编译打开语法糖,就看到了枚举类型的本质,简化如下:

// 枚举
// ThreadSafe
public class Singleton4 extends Enum<Singleton4> {
  ...
  public static final Singleton4 SINGLETON = new Singleton4();
  ...
}

本质上和饿汉模式相同,区别仅在于公有的静态成员变量。

综上所述,一般使用的是 双重检查锁 和 静态内部类来实现单例模式。


为帮助开发者们提升面试技能、有机会入职BATJ等大厂公司,特别制作了这个专辑——这一次整体放出。

大致内容包括了: Java 集合、JVM、多线程、并发编程、设计模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat等大厂面试题等、等技术栈! 

需要获取以下这些面试题答案以及学习资料得话麻烦关注+好评之后

直接点击此链接→【点我直接获取】  即可免费获取哦~~

ÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÂ¥ÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃèÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃèÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÿÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃéÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃæÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÂ¥ÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÂ¥ÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÂ¥ÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃþÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃçÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃæÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃèÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÿÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃð


看完三件事❤️

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注作者公众号 『 java烂猪皮 』,不定期分享原创知识。
  • 关注后回复【666】扫码即可获取学习资料包。
  • 同时可以期待后续文章ing🚀。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值