这个面试题很简单,其实就是考查小伙伴儿们对单例模式的理解和运用。单例模式嘛,就是保证一个类只有一个实例,并提供一个全局访问点。但是单例模式的实现方式可是有很多种,想必每个小伙伴儿在新手村时就听说过“饿汉式”和“懒汉式”单例吧!
今天,我们就从牛马的工作中停下来喘口气,一起来盘点下 Java 中几种单例模式的写法。
首先,最简单粗暴的写法当属饿汉式单例。看代码:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
// 私有构造防止外部随便 new 对象
}
public static Singleton getInstance() {
return instance;
}
}
这种方式就是在类加载时直接初始化实例,从此高枕无忧。线程安全是它的一大卖点,毕竟加锁啥的都不用搞。但缺点也明摆着,资源利用率不高,就算你压根不用这个实例,它也会在你的内存里躺着。
那有没有啥办法能懒一点,等到真正要用时再创建实例呢?
有!懒汉式单例应运而生。基本版本长这样:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 执行到这里,发现还没创建对象
instance = new Singleton(); // 赶紧创建一个
}
return instance;
}
}
乍一看这种模式简直是非常地 smart 啊,只在需要时创建实例。但别高兴太早,它有个致命缺陷——线程不安全。想象一下,两个线程同时跑到 instance == null
这句,都以为自己是“第一个”,各自创建了一个实例,妥妥的悲剧,说好的单例呢?。
那咋办?加锁呗!于是就有了懒汉式单例的线程安全版:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
synchronized
加在方法上,保证同一时间只能有一个线程进入,问题解决!但是这样性能又有点拉胯,每次调用都得加锁,哪怕实例已经创建好了,这事儿办得有点费劲。
好在,大神们又想出个绝妙的优化方案——双检锁单例(DCL,即 double-checked locking)。代码如下:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,没锁快得很
synchronized (Singleton.class) { // 加个锁,进入“安全区”
if (instance == null) { // 第二次检查,确保安全
instance = new Singleton(); // 安心创建
}
}
}
return instance;
}
}
这套路真是绝了。第一检查没锁,速度快;一旦检测到需要创建实例,就加把锁,进入“安全区”再认真检查一遍。 instance 前面加的 volatile
也有讲究,防止指令重排序。
为什么需要 volatile 关键字?
在 Java 中,创建对象的过程分为三个步骤:
分配内存空间:为新对象分配一块内存。 初始化对象:调用构造方法初始化对象。 设置引用指向内存:将对象的引用指向分配的内存地址。 在单线程环境下,这三步是按顺序执行的。但在多线程环境下,JVM 可能会对指令进行重排序,优化执行效率。例如,步骤 2 和步骤 3 可能会被重排序为 1-3-2。
假设线程 A 执行到
instance = new Singleton();
时,由于指令重排序,可能先分配内存,然后将引用指向内存地址,但还没有初始化对象。此时,线程 B 进入getInstance()
方法,发现instance
不为null
,直接返回实例。但这个实例可能还没有完成初始化,导致后续操作出现问题。
volatile 关键字的作用
禁止指令重排序: volatile
关键字可以防止 JVM 对指令进行重排序,确保创建对象的三步操作按顺序执行。保证内存可见性: volatile
关键字确保当一个线程修改了instance
的值,其他线程能够立即看到这个修改。
通过 volatile
关键字,双检锁单例模式在多线程环境下能够确保线程安全,同时避免了每次调用都加锁的性能损耗。
不过,还有一种更优雅的实现方式,那就是静态内部类单例。咱瞧瞧:
public class Singleton {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
这利用了 Java 的类加载机制,只有在第一次用到 SingletonHolder.instance
时才会加载内部类,从而创建实例。它线程安全,还不需要锁,性能杠杠的,真香!
最后一种,隆重介绍一下:枚举单例。如果不需要额外的功能,枚举单例绝对是首选。看代码:
public enum Singleton {
INSTANCE;
public void anyMethod() {
}
}
枚举单例的实现非常简单,只需要定义一个枚举实例,比如 INSTANCE
,就完成了单例的实现。JVM 会自动处理实例的创建和初始化,开发者不需要额外写代码来保证线程安全。
为什么枚举单例是绝对线程安全的?
枚举类型在 Java 中是特殊的类,由 JVM 保证其线程安全性。枚举类在第一次被引用时,会进行类加载和初始化。
类加载过程是线程安全的,JVM 确保同一个类只会被加载和初始化一次。
好了,最后来总结一下这几种方式的优缺点:
-
饿汉式:简单粗暴,线程安全,但实例创建早,浪费资源。 -
懒汉式(非线程安全版):懒加载,但线程不安全,没啥可取之处。 -
懒汉式(线程安全版):解决了线程问题,但性能差。 -
双检锁:线程安全,性能好,但代码稍微复杂。 -
静态内部类:线程安全,懒加载,性能好,推荐! -
枚举单例:最优雅,最安全,最省心。
具体用哪种,得看场景。如果项目追求简洁高效,双检锁或静态内部类是首选;如果项目里反射和序列化是常客,那非枚举单例莫属。
好啦,关于 Java 中的单例模式实现方式就聊到这儿,希望能帮到你哦!
您的鼓励对我持续创作非常关键,如果本文对您有帮助,请记得点赞、分享、在看哦~~~谢谢!
原文始发于微信公众号(Java驿站):美团一面:双检锁单例会写吗?
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/312812.html