【问题标题】:Singleton without volatile member没有 volatile 成员的单例
【发布时间】:2015-05-09 20:28:13
【问题描述】:

假设我有下一节课:

public class Singleton{

  private static Singleton _instance; 

  public static Singleton getInstance(){

   if(_instance == null){
         synchronized(Singleton.class){
           if(_instance == null)
           _instance = new Singleton();
         }
   }
   return _instance;
 }

例如,我们有两个线程 AB 尝试同时执行 getInstance() 方法。我是否正确理解了这个过程:


  1. 线程A进入getInstance()方法并获取锁;
  2. 线程B也进入getInstance()方法并阻塞;
  3. 线程A 创建新的Singleton() 对象并释放锁,现在_instance 变量的last 值应该对线程B?或者线程 B 仍然可以拥有自己的 _instance 变量副本,该副本与主内存不同步(_instance=null)?

【问题讨论】:

    标签: java multithreading


    【解决方案1】:

    线程 B 在同步时被阻塞,当它继续进行时,它将看到更改的字段_instance != null,因此不构造一个,而是返回现有的。

    稍后出现的所有其他线程都会看到正在设置的实例,甚至不会锁定。

    问题:您的代码不完整,您需要 volatile 以确保未通过同步的线程(希望大多数线程)仍然只能看到完全发布的单例对象。

    Java 内存模型仅保证最终字段已初始化。对于所有其他人,您需要safe publish,这可以通过:

    • 通过正确锁定的字段交换引用 (JLS 17.4.5)
    • 使用静态初始化器进行初始化存储 (JLS 12.4)
    • 通过 volatile 字段 (JLS 17.4.5) 或作为此规则的结果,通过 AtomicX 类交换引用
    • 将值初始化为最终字段 (JLS 17.5)。

    避免易失性(或也可以安全地将对象发布到其他线程的原子引用)的最简单方法是使用正常的对象初始化,这是 JVM 提供的有效且健壮的单例(但不是惰性):

    class Singleton
    {
        private static final Singleton HIGHLANDER = new Singleton();
        private Singleton() { } // not accessible
        public static getSingleton() { return HIGHLANDER; }
    }
    

    JDK 在内部使用这种与 "Holder" objects 类似的构造来实现同样简单而健壮的模式,但以一种惰性的方式:

    class Singleton
    {
        private Singleton() { } // not accessible
        private static Class LazyHolder {
            private static final Singleton LAZY_HIGHLANDER = new Singleton();
        }
        public static Singleton getInstance() {
            return LazyHolder.LAZY_HIGHLANDER;
        }
    }
    

    这两种方法都不需要volatile 变量访问(在 DCL 情况下您需要)或同步(它由 JVM 隐式完成,由类锁保护初始化)。

    【讨论】:

    • 如果我先将 if 放入同步块,那么在这种情况下,所有线程都应该看到完全发布的单例对象?
    • 是的,这是经典的单例模式,它总是安全的。但它的可扩展性很差,因为每次访问都会锁定工厂。它肯定比具有多个线程的 volatile 慢。 (这就是为什么您通常需要 DCL 或我展示的解决方案之一)。
    【解决方案2】:

    您在此处显示的内容称为double-checked locking

    静态变量属于,而不是线程。两个线程都会看到正确的值,但编译器可能会优化读取,以便不会两次检查静态变量。为此,you should declare the variable with the volatile keyword

    请注意,在版本 5 之前的 Java 版本中,即使使用 volatile 变量,这也可能无法正常工作。过去,分配可以将部分构造的对象分配给变量。现在构造函数必须在赋值可以继续之前返回。这将在任何现代版本的 Java 中正常工作。

    【讨论】:

    • 即使使用 Java 5 DCL 也要求该字段是可变的,或者您需要任何其他机制(来自 java.util.concurrent)来确保安全发布。
    • @eckes 你是对的,原来volatile 是必需的。考虑到我使用单例的频率(并且在我确实需要单例的极少数情况下更喜欢在加载类时初始化它们),我错过了那个细节。
    • 我认为对 volatile 的解释是不完整的,变量实际上已正确检查进入同步的线程(这将是所有看不到新引用的线程)但是事实上任何以后的线程确实看到引用并不意味着被引用的对象在那个时候是完全构造的 - volatile 将确保这一点。
    • @eckes 在一个线程分配变量之后,另一个线程可能不会立即看到它如果它已经在方法中。但是,Java 5 及更高版本会确保在分配变量之前完全构造对象。
    • 是的,这是可能的,但如果它在第一个 if 之前,它将进入同步(因为它是 null),如果它在第一个 if 之后,它也会已经在同步中(因为它是null)。线程不可能看到空字段而不走慢速路径(这将同步,因此是安全的)。只要您不安全地发布它,Java 5 将无法确保对象完全构造为其他线程所见。
    【解决方案3】:

    据我所知,可能存在两个问题。 线程 B 可能会也可能不会看到最新值。 线程 B 可能会看到一个部分构造的对象,以防构造函数做了很多事情,而 JVM 决定更改代码的顺序。

    使其 volatile 解决了这两个问题,因为它强制发生之前的关系并阻止 JVM 重新排序代码执行,并更新其他线程中的值。

    【讨论】:

      猜你喜欢
      • 2011-05-27
      • 1970-01-01
      • 2019-05-27
      • 1970-01-01
      • 2020-05-05
      • 1970-01-01
      • 2020-12-04
      • 1970-01-01
      • 2021-05-24
      相关资源
      最近更新 更多