【问题标题】:Is Java eager singleton creation thread safe?Java 急切的单例创建线程安全吗?
【发布时间】:2018-10-07 11:20:46
【问题描述】:

我喜欢 java 中“渴望单例”的简单性,大多数关于它的文章都称其创建线程安全。

class Singleton {

public static final Singleton instance = new Singleton ();

    private Singleton (){};

    public static Singleton getInstance(){

        return instance;

    }
}

但是,我听到一些声称它的创建可能不是线程安全的。例如,一位消息人士声称,如果使用超过 1 个类加载器或 App 域,则不安全。

JVM 是否保证“Eager Singleton”的创建是线程安全的,例如,2 个线程不会意外地同时创建单例?

编辑: 对象创建的线程安全是否需要关键字 final?如果字段不是最终的,它不是线程安全的吗?

【问题讨论】:

  • 我听说过一些说法...来源???
  • 如果我们不是在谈论synchronized,我们就不是在谈论线程安全性。它们是网络上的大量示例,例如:journaldev.com/1377/…
  • @maspinu 不知道您所说的“如果我们不是在谈论同步,我们不是在谈论线程安全性”是什么意思,但是您发布的链接证实了 OP 急切的单例模式使用是安全的(这是正确的)
  • 如果你有多个类加载器在运行,你可以有多个不同版本的代码,正如你所期望的那样。

标签: java multithreading singleton


【解决方案1】:

您使用的方法是线程安全的。由于您没有引用您正在谈论的主张,因此我无法直接解决它们。 但是 Java 语言规范对这个主题很清楚。

section 17.5 中描述

final 字段还允许程序员实现线程安全的不可变 没有同步的对象。 线程安全的不可变对象是 被所有线程视为不可变的,即使使用数据竞争来传递 对线程之间不可变对象的引用。这可以提供 安全保证不会被不正确的或不可变的类误用 恶意代码。必须正确使用最终字段以提供 保证不变性。

一个对象被认为是完全初始化的,当它 构造函数完成。只能看到对 保证该对象完全初始化后的对象 查看该对象最终的正确初始化值 字段。

【讨论】:

  • 这里只讨论了问题的不变性。他们将“一个只能在对象完全初始化后才能看到对该对象的引用的线程”作为条件编写,而不是作为事实的保证。
  • @Gonan:这就是你要求的。创建是完全线程安全的。现在,如果实现被破坏,那是另一个问题。拥有多个类加载器的问题也不是线程安全问题。
  • @GonenI 我不知道规范如何更清晰:“保证可以看到”
  • 如果这是一个实例初始化器,我的回答会很明显:​​“不”。您需要 final(或synchronized),否则无法安全发布。使用静态初始化器有点棘手。我不知道有任何规范可以保证它会起作用,但是我尝试使用 ClassLoaders 和静态初始化程序创建一个竞争条件,但我失败了 - 您可能总是会在某个时候遇到 synchronzied 块类加载器也被执行静态初始化的类加载器命中,确保安全发布。但未来版本的 JVM 可能不会
  • @ErwinBolwidt 保证静态初始化器 are precisely specified。注意“初始化锁”的出现。
【解决方案2】:

我不认为“渴望单身”这个名字是合理的。

考虑JLS §12.4.1., When Initialization Occurs

类或接口 T 将在以下任何一项第一次出现之前立即初始化:

  • T 是一个类,并创建了一个 T 的实例。
  • 调用T 声明的static 方法。
  • 分配了T 声明的static 字段。
  • 使用了由T 声明的static 字段,并且该字段不是常量变量(§4.12.4)。

因此,当第一次调用方法 getInstance() 时,或者由于您创建了字段 public,当第一次访问该字段时,将发生初始化,进而实例化 Singleton时间,以先到者为准。 但不是更早。换句话说,这个初始化已经和所有其他尝试执行惰性初始化的尝试一样惰性。

这个初始化的安全性由JLS §12.4.2, Detailed Initialization Procedure给出

对于每个类或接口C,都有一个唯一的初始化锁 LC。从CLC 的映射由管理员自行决定 Java 虚拟机实现。初始化C 的过程如下 如下:

  1. C 的初始化锁LC 上同步。这涉及等到当前线程可以获取LC
  2. 如果CClass 对象表明其他线程正在对C 进行初始化,则释放LC 并阻塞当前线程,直到通知正在进行的初始化已完成,在哪次重复这一步。
  3. 如果CClass 对象表明当前线程正在对C 进行初始化,那么这一定是一个递归的初始化请求。释放LC并正常完成。
  4. 如果CClass 对象表明C 已被初始化,则无需进一步操作。释放LC,正常完成。

这是一个过程,每次解析对类的引用,获取类的唯一初始化锁,并在类已经初始化或当前线程是执行初始化的线程时释放它。这个锁保证了线程的安全,不管这个字段是否被声明为final,只要它只在类初始化时写入。

为什么它仍然是实现单例的最有效方式的原因在同一章中给出:

当一个实现可以确定类的初始化已经完成时,可以通过省略步骤 1 中的锁获取(并在步骤 4/5 中释放)来优化此过程,前提是,在内存模型中,所有在获得锁时会存在的先发生顺序,在执行优化时仍然存在。

由于每个类只初始化一次,然后在这个初始化状态下使用比它的初始化时间很长的时间,这种优化具有非常高的影响,因此,自第一个 Java 版本以来,它是最先进的。但是,正如这篇笔记所说,优化不能破坏线程安全

【讨论】:

  • 同意。您将很难找到这种单例比其他单例方法更懒惰的现实世界案例。
【解决方案3】:

即使我们从public static final Singleton instance = new Singleton (); 中删除final,它也是线程安全的。

这是因为 JVM 保证在任何线程访问静态 instance 变量之前创建实例。

【讨论】:

    猜你喜欢
    • 2017-04-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多