【翻译】如何在Java中编写线程安全的单例 - Java单例示例

英文原文:How to create thread safe Singleton in Java - Java Singleton Example

线程安全的单例指的是即使处于多线程环境,也总是返回完全相同实例的单例类。在Java中,单例设计模式就像工厂方法设计模式或装饰器设计模式那样,已经是一种经典的设计模式,甚至在JDK内部也用得很多,例如java.lang.runtime就是单例类的一个例子。单例设计模式确保了Java程序中的类在任意时间内只保持唯一一个实例。

在我们的上一篇文章“10个关于Java单例的面试问题”中,我们已经讨论了许多不同的有关单例设计模式在面试中被问及的问题,其中一个写的就是关于线程安全的。在Java 5版本之前,用于编写线程安全单例的“双重检查锁”机制在这种情况下将会失效,如果一个线程没有看到另一个并发线程创建的实例,最终将会导致单例类的多个实例被创建。[1]从Java 5版本开始,volatile型变量保证了可以通过“双重检查锁”模式编写线程安全的单例。

我个人不喜欢“双重检查锁”这种方法,因为还有很多其他更简单的用于编写线程安全单例的替代方法可以使用,像使用静态变量来初始化单例类实例,或使用枚举作为单例。让我们来看一下这两种编写线程安全单例方法的例子。

Java单例示例 - 使用枚举实现线程安全的单例

我写作“Java枚举的10个例子”时,这个例子还没有被列举出来。到目前为止,在Java中使用枚举编写线程安全的单例是最简单和有效的方法。因为Java编程语言它自身就能提供线程安全的保证。你不需要担心线程安全问题。因为在Java中枚举实例默认是final修饰的,它也保证不会由于序列化导致出现多个实例。[2]

有一点值得牢记的是,当我们讨论线程安全的单例,我们讨论的是在单例类的实例在创建期间的线程安全,而不是当我们调用单例类的任意方法时的线程安全。如果你的单例类维护了任意状态信息,并且包含了修改该状态的方法,那么你需要编写避免出现线程安全和同步问题的代码。下面是使用枚举在Java中实现线程安全的单例的例子。如果这个办法满足你的需求,那么这是在Java中编写线程安全单例最简单的办法了。使用枚举作为单例也提供了其他几个好处,你可以在“为什么在Java中用枚举实现单例更好一些”找到原因。

public enum Singleton{
    INSTANCE;
  
    public void show(){
        System.out.println("Singleton using Enum in Java");
    }
}

//你可以通过Singleton.INSTANCE访问该单例类的实例,并按如下方式调用该实例的任意方法
Singleton.INSTANCE.show();

Java单例示例 - 使用静态变量初始化实现线程安全的单例

在Java中,你也可以通过使得在类的加载过程中就创建单例类实例的办法来编写线程安全的单例。在类的加载过程中,静态变量会被初始化,类加载器将保证实例在完全创建之前是不可见的。下面是在Java中使用静态工厂方法编写线程安全单例的例子。使用静态变量实现单例设计模式的唯一缺点就是它不是延迟加载的,甚至在任意客户端调用getInstance()方法之前,单例类就已经初始化了。

public class Singleton{
    private static final Singleton INSTANCE = new Singleton();
  
    private Singleton(){ }

    public static Singleton getInstance(){
        return INSTANCE;
    }
    public void show(){
        System.out.println("Singleon using static initialization in Java");
    }
}

//按如下方式访问该单例类的实例,并调用该实例的任意方法
Singleton.getInstance().show();

我们不用在getInstance()方法内部创建单例类的实例,而是它会被类加载器创建。同样,除了仅有的一个实例之外,私有化的构造方法使得创建其他实例是不可能的。你仍然可以通过反射和调用setAccessible(true)方法来访问私有化的构造方法。不过你可以通过在构造器中抛出异常来阻止这种情况导致新实例的创建。

进一步了解
Java中的多线程和并行计算
Java并发实战(书籍)
将并发性和多线程应用于常见的Java设计模式
Java并发实践课程

这两种都是编写线程安全单例的方法,不过我个人更喜欢使用枚举。因为它简单,能防止多重实例序列化攻击并且代码简洁。

在Javarevisited博客中的其他Java设计模式教程
Java中的观察者设计模式和在真实世界中的相关例子
设计模式和软件设计的面试问题
Java程序员的10个OOPS和SOLID设计原则
Java中的建造者设计模式和相关例子
为什么在Java的Object类中有wait方法和notify方法


脚注


  1. “在Java 5版本之前,用于编写线程安全单例的“双重检查锁”机制在这种情况下将会失效,如果一个线程没有看到另一个并发线程创建的实例,最终将会导致单例类的多个实例被创建。”
    这句话存在问题,应该不是多个实例被创建,而是仅有一个实例被创建,不过如果没有用volatile关键字,可能会导致异常的发生。参见:双重检查锁方法 ↩︎

  2. “因为在Java中枚举实例默认是final修饰的,它也保证不会由于序列化导致出现多个实例。”
    没明白这句话表达的意思。 ↩︎

comments powered by Disqus