Java单例模式线程安全问题与解决方案详解

单例是最广泛使用的创建性设计模式之一,用于限制应用程序创建的对象。如果在多线程环境中使用它,则单例类的线程安全性非常重要。在现实世界的应用程序中,像数据库连接或企业信息系统(EIS)这样的资源是有限的,应该明智地使用以避免资源紧缺。为了实现这一点,我们可以实现一个单例设计模式。我们可以为资源创建一个包装类,并在运行时限制对象的数量为一个。

在Java中的线程安全单例模式

Java中的线程安全单例
  1. 创建私有构造函数以避免使用新操作符创建任何新对象。
  2. 声明一个私有静态类实例。
  3. 提供一个公共静态方法,用于返回单例类的实例变量。如果变量未初始化,则进行初始化,否则简单地返回实例变量。

使用上述步骤,我已创建一个类似以下样式的单例类。ASingleton.java

package com.Olivia.designpatterns;

public class ASingleton {

    private static ASingleton instance = null;

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

    public static ASingleton getInstance() {
        if (instance == null) {
            instance = new ASingleton();
        }
        return instance;
    }

}

在上述代码中,getInstance()方法不是线程安全的。多个线程可以同时访问它。对于前几个访问该实例变量尚未初始化的线程,多个线程可以进入if循环并创建多个实例。这将破坏我们的单例实现。

如何在单例类中实现线程安全?

我们可以通过三种方式实现线程安全。

  1. 在类加载时创建实例变量。

    优点:

    • 无需同步即可保证线程安全
    • 实现简单

    缺点:

    • 提前创建资源,可能在应用程序中不会被使用
    • 客户端应用程序无法传递任何参数,因此我们不能重用它。例如,为数据库连接创建一个通用单例类,其中客户端应用程序提供数据库服务器属性
  2. 同步getInstance()方法。

    优点:

    • 保证线程安全
    • 客户端应用程序可以传递参数
    • 实现了延迟初始化

    缺点:

    • 由于锁定开销导致性能较慢
    • 一旦实例变量被初始化,就不需要不必要的同步
  3. 在if循环内部使用同步块和volatile变量

    优点:

    • 保证线程安全
    • 客户端应用程序可以传递参数
    • 实现了延迟初始化
    • 同步开销最小,并且仅适用于变量为null时的前几个线程

    缺点:

    • 额外的if条件

观察了实现线程安全的三种方式后,我认为第三种方式是最好的选择。在这种情况下,修改后的类将会是这样的。

package com.Olivia.designpatterns;

public class ASingleton {

    private static volatile ASingleton instance;  // 使用volatile确保多线程环境下instance变量的可见性
    private static Object mutex = new Object();    // 互斥锁对象,用于同步块

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

    public static ASingleton getInstance() {
        ASingleton result = instance;  // 使用局部变量减少对volatile变量的访问
        if (result == null) {
            synchronized (mutex) {      // 使用同步块确保线程安全
                result = instance;
                if (result == null)
                    instance = result = new ASingleton();
            }
        }
        return result;
    }

}

本地变量result似乎是不必要的。但是,它存在是为了提高我们代码的性能。在实例已经被初始化的情况下(大多数情况下),volatile字段只被访问一次(因为使用了”return result;”而不是”return instance;”)。这可以将方法的整体性能提高25%。如果您认为有更好的方法来实现这一点,或者以上实现威胁到线程安全,请评论并与我们分享。

额外小贴士

字符串不是使用synchronized关键字的很好的选择,原因是它们存在于字符串池中,我们不希望锁定一个可能被其他代码使用的字符串。因此,我使用一个Object变量。学习更多关于Java中同步和线程安全的知识。

bannerAds