Java深入解析:线程安全的单例模式实现

啥是设计模式?

设计模式好比象棋中的 “棋谱”。红方当头炮,黑方马来跳。针对红方的一些走法,黑方应招的时候有一些固定的套路。按照套路来走局势就不会吃亏。

软件开发中也有很多常见的 “问题场景”。针对这些问题场景,大佬们总结出了一些固定的套路。按照这个套路来实现代码,也不会吃亏。

一、饿汉式:简单粗暴的线程安全​

饿汉式单例模式的核心思路是在类加载时就完成实例的创建,将实例化过程提前,从而避免多线程环境下可能出现的并发问题。Java 代码如下:

public class Singleton {
    // 类加载时立即初始化实例
    private static final Singleton instance = new Singleton();

    // 私有化构造函数,防止外部实例化
    private Singleton() {}

    // 提供全局访问点
    public static Singleton getInstance() {
        return instance;
    }
}

在上述代码中,instance 在类加载阶段就被赋值为 Singleton 类的实例。由于 Java 中类加载机制是线程安全的,当多个线程同时访问 getInstance 方法时,获取到的始终是同一个实例,不存在多个线程同时创建实例的情况。​

然而,这种方式的弊端也很明显。即使程序暂时不需要使用单例实例,实例也会在类加载时被创建,这可能造成内存资源的浪费。因此,饿汉式单例模式更适用于实例创建开销较小,且程序启动时就需要使用该实例的场景。

二、懒汉式:双重检查锁的精妙平衡

类加载的时候不创建实例。第一次使用的时候才创建实例。

Java单线程版代码如下:

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

上面的懒汉模式的实现是线程不安全的。

线程安全问题发生在首次创建实例时,如果在多个线程中同时调用getInstance方法,就可能导致创建出多个实例。

一旦实例已经创建好了,后面再多线程环境调用getInstance就不再有线程安全问题了(不在修改instance了)

加上synchronized可以改善这里的线程安全问题。

Java多线程版代码如下:

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

上面的多线程代码仍有改进的空间,一下代码在枷锁的基础上,做出了进一步改动:

使用双重if判定,降低锁竞争的频率/

给instance加上了volatile。

class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

理解双重if判定/volatile:

加锁/解锁是一件开销比较高的事情,而懒汉模式的线程不安全只是发生在首次创建实例的时候,因此后续使用的时候,不必在进行加锁了。

外层的 if 就是判定下当前是否已经把instance实例创建出来了。

同时为了避免"内存可见性"导致读取的instance出现偏差,于是补充说voiatile。

当多线程首次调用getinstance,大家可能都发现instance为null,于是又继续往下执行来竞争锁,其中竞争成功的线程,在完成创建实例的操作。

当这个实例创建完成了之后,其他竞争到锁的线程就被里层 if 挡住了,也就不会继续创建其他实例。

1.有三个线程,开始执行getInstance,通过外层的if(instance == null)知道了实例还没有创建的消息,于是开始竞争同一把锁。

2.其中线程1率先获取到锁,此时线程1通过里层的 if (instance ==null) 进一步确认实例是否已经创建,如果没创建,就把这个实例创建出来。

3.当线程1释放锁之后,线程2和线程3也拿到锁,也通过里层的 if (instance == null)来确认实例是否已经创建,发现实例已经创建出来了,就不再创建了。

4.后续的线程,不必加锁,直接就通过外层if (instance == null)就知道实例已经创建了,从而不再尝试获取锁了,降低了开销。

三、静态内部类:JVM 保驾护航的优雅方案

静态内部类方式在 Java 中应用广泛,它巧妙地利用了 JVM 的类加载机制来实现线程安全与延迟初始化的完美结合。Java 代码示例如下:

public class Singleton {
    // 私有化构造函数
    private Singleton() {}

    // 静态内部类,在外部类被加载时不会被加载
    private static class SingletonHolder {
        // 静态常量,在内部类被加载时创建实例
        private static final Singleton INSTANCE = new Singleton();
    }

    // 提供全局访问点
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

在上述代码中,Singleton 类的构造函数被私有化,防止外部直接实例化。SingletonHolder 是 Singleton 的静态内部类,其中定义了静态常量 INSTANCE 。当 Singleton 类被加载时,SingletonHolder 类并不会被立即加载,只有当调用 getInstance 方法时,SingletonHolder 类才会被加载,此时 INSTANCE 会被创建。由于 JVM 保证类加载过程的线程安全,所以静态内部类方式既保证了线程安全,又实现了延迟初始化,是一种非常优雅的实现方式 。

四、枚举:简洁高效的终极方案

在 Java 中,使用枚举来实现单例模式是一种非常简洁且安全的方式,代码如下:

public enum Singleton {
    INSTANCE;
    // 可以在枚举中定义其他方法和属性
    public void doSomething() {
        System.out.println("Singleton instance is doing something.");
    }
}

在上述代码中,Singleton 枚举类型仅有一个成员 INSTANCE ,它就是单例实例。枚举类型天生支持线程安全,并且自动处理了序列化和反序列化问题,防止通过反序列化创建多个实例。在使用时,直接通过 Singleton.INSTANCE 访问实例,非常方便。此外,还可以在枚举中定义其他方法和属性,进一步扩展单例的功能。​

五、总结与选择建议​

以上介绍了多种 Java 线程安全的单例模式实现方式,每种方式都有其独特的优势和适用场景。饿汉式简单直接,但可能造成资源浪费;懒汉式双重检查锁实现了性能与安全的平衡,但需要正确使用 volatile 关键字;静态内部类借助 JVM 实现优雅的线程安全与延迟初始化;枚举方式简洁高效,尤其适合处理序列化场景。​

在实际项目中,开发者应根据具体需求选择合适的单例模式实现方式。如果对资源消耗不敏感,追求简单性,饿汉式是不错的选择;若需要延迟初始化且注重性能,懒汉式双重检查锁或静态内部类更为合适;而对于涉及序列化和反序列化的场景,枚举方式则是最佳实践。掌握这些单例模式的实现技巧,将有助于我们编写出更加健壮、高效的 Java 代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值