单例模式五种写法与线程安全

    技术2026-04-24  12

    单例模式五种写法与线程安全

    懒汉式

    是否 Lazy 初始化:是

    是否多线程安全:可以实现

    实现难度:易

    优点:第一次调用才初始化,避免内存浪费。

    缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

    单线程:

    public class LazyMan { //线程不安全 private LazyMan(){ } private static LazyMan instance; public static LazyMan getInstance(){ if (instance==null){ instance = new LazyMan(); } return instance; } }

    多线程:

    public class LazyManSafe { //线程安全,但是锁占用资源 private LazyManSafe(){ } private static LazyManSafe instance; public synchronized static LazyManSafe getInstance(){ if (instance==null){ instance = new LazyManSafe(); } return instance; } }

    饿汉式

    是否 Lazy 初始化:否

    是否多线程安全:是

    实现难度:易

    优点:没有加锁,执行效率会提高。

    缺点:类加载时就初始化,浪费内存。

    public class HungryMan { //线程安全,但是浪费资源空间 private HungryMan(){ } //注意加上final private static final HungryMan instance = new HungryMan(); public static HungryMan getInstance(){ return instance; } }

    它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

    双重锁/双重校验锁

    JDK 版本:JDK1.5 起

    是否 Lazy 初始化:是

    是否多线程安全:是

    实现难度:较复杂

    public class DoubleCheck { private DoubleCheck(){ } //注意加上volatile private volatile static DoubleCheck instance; public static DoubleCheck getInstance(){ if (instance==null){ synchronized (DoubleCheck.class){ if (instance==null){ instance = new DoubleCheck(); } } } return instance; } }

    1.双重判断的原因:

    第一次:避免不必要的线程等待,相当于对多线程懒汉式的优化,提高效率

    第二次:避免多次实例化,单例模式的基本检查

    2.使用volatile的原因:

    主要是禁止重排序,初始化一个实例(SomeType st = new SomeType())在java字节码中会有4个步骤:

    1、申请内存空间 2、初始化默认值(区别于构造器方法的初始化) 3、执行构造器方法 4、连接引用和实例

    这4个步骤后两个有可能会重排序,1234或1243都有可能,造成未初始化完全的对象发布。volatile可以禁止指令重排序,从而避免这个问题。

    3.为什么是JDK1.5起?

    (待补充,大致是因为JVM虚拟机、volatile在jdk1.5前后有改变)

    4.为什么synchronized保证有序性,却还要用volatile?

    关于这个问题,可以参考知乎Java synchronized 能防止指令重排序吗?

    静态内部类

    是否 Lazy 初始化:是

    是否多线程安全:是

    实现难度:一般

    //静态内部类 public class Holder { private Holder(){ } static class InnerHolder{ private static final Holder instance = new Holder(); } public static Holder getInstance(){ return InnerHolder.instance; } }

    只有第一次调用getInstance方法时,虚拟机才加载 内部类 并初始化 instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。

    如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

    枚举

    JDK 版本:JDK1.5 起

    是否 Lazy 初始化:否

    是否多线程安全:是

    实现难度:易

    public enum Singleton { INSTANCE; }

    默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。实际上

    枚举类隐藏了私有的构造器。枚举类的域 是相应类型的一个实例对象 那么枚举类型日常用例是这样子的: public enum Singleton { INSTANCE //doSomething 该实例支持的行为 //可以省略此方法,通过Singleton.INSTANCE进行操作 public static Singleton get Instance() { return Singleton.INSTANCE; } }

    这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。 不能通过 reflection attack 来调用私有构造方法。

    结论:

    一般情况下,不建议使用第 1 种懒汉方式,建议使用第 2 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。

    对于第3种双重锁单例模式,本人认为加了volatile后应该解决了问题,可以算的上是一个线程安全、高效、懒加载的单例模式,但是搜索网上评论后大多都没有推荐这种方式,如有读者知道原因,请在评论区指出。

    Processed: 0.008, SQL: 9