出版和阅读非易变性领域

时间:2017-12-04 17:04:24

标签: java volatile

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

这几乎是正确的线程安全Singleton的实现。我看到的唯一问题是:

初始化thread #1字段的instance可以在完全初始化之前发布。现在,第二个线程可以在不一致的状态下读取instance

但是,就我而言,这里只有 问题。这只是问题吗? (我们可以使instance不稳定。)

7 个答案:

答案 0 :(得分:6)

您的例子由Shipilev在Safe Publication and Safe Initialization in Java中解释。我强烈建议您阅读整篇文章,但总结一下,请查看UnsafeLocalDCLFactory部分:

public class UnsafeLocalDCLFactory implements Factory {
  private Singleton instance; // deliberately non-volatile

  @Override
  public Singleton getInstance() {
    Singleton res = instance;
    if (res == null) {
      synchronized (this) {
        res = instance;
        if (res == null) {
           res = new Singleton();
           instance = res;
        }
      }
    }
    return res;
  }
}

以上有以下问题:

  

这里引入局部变量是一个正确性修正,但只是部分:在发布Singleton实例和读取其任何字段之前仍然没有发生过。我们只是保护自己不回来" null"而不是Singleton实例。同样的技巧也可以被视为SafeDCLFactory的性能优化,即只进行一次易失性读取,产生:

Shipilev建议通过标记instance volatile:

来解决如下问题
public class SafeLocalDCLFactory implements Factory {
  private volatile Singleton instance;

  @Override
  public Singleton getInstance() {
    Singleton res = instance;
    if (res == null) {
      synchronized (this) {
        res = instance;
        if (res == null) {
          res = new Singleton();
          instance = res;
        }
      }
    }
    return res;
  }
}

此示例没有其他问题。

答案 1 :(得分:3)

这是一个很好的问题,我将在这里总结一下我的理解。

假设Thread1当前正在初始化Singleton实例并发布引用(显然不安全)。 Thread2可以看到这个不安全的已发布引用(意味着它看到非空引用),但这并不意味着它通过该引用看到的字段(Singleton字段通过构造函数)也正确初始化。

据我所知,这是因为可能会重新排序构造函数中发生的字段的存储。因为没有"发生之前"规则(这些是简单的变量),这可能是完全可能的。

但这不是唯一的问题。请注意,您在此处执行两次读取操作:

if (res == null) { // read 1

return res // read 2

这些读取没有同步保护,因此这些是粗略的读取。 AFAIK这意味着允许读取1读取非空引用,而允许读取2读取空引用。

这个顺便说一下ALL mighty Shipilev explains(即使我读了这篇文章一年半,我每次都会发现新的东西)。

确实使实例volatile可以解决问题。当你使它变得不稳定时,就会发生这种情况:

 instance = res; // volatile write, thus [LoadStore][StoreStore] barriers

所有"其他" actions(构造函数内的商店)无法通过此fence,也不会重新排序。这也意味着当你读取volatile变量并看到一个非null值时,就意味着每一个" write"在写出volatile本身之前已经完成了肯定。 This excellent post has the exact meaning of it

这也解决了第二个问题,因为无法重新排序这些操作,所以您可以确保从read 1read 2看到相同的值。

无论我阅读多少并尝试理解这些东西对我来说都是复杂的,我知道很少有人可以编写这样的代码并且也可以正确地理解它。当你可以(我做!)时,请坚持双重检查锁定的已知和工作实例:)

答案 2 :(得分:3)

通常情况下,我再也不会使用双重检查锁定机制了。要创建线程安全单例,您应该让编译器执行此操作:

tuple

现在你正在谈论使实例变得不稳定。我不认为这个解决方案是必要的,因为jit编译器现在可以在构造对象时处理线程的同步。但如果你想让它变得不稳定,你可以。

最后,我会将getInstance()和实例设为静态。然后,您可以直接引用Factory.getInstance()而无需构造Factory类。另外:您将在应用程序的所有线程中获得相同的实例。否则,每个新的Factory()都会给你一个新实例。

您还可以查看维基百科。如果你需要一个懒惰的解决方案,他们有一个干净的解决方案:

https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java

public static String getDate(long time) {
    Calendar cal = Calendar.getInstance(Locale.ENGLISH);
    cal.setTimeInMillis(time);
    SimpleDateFormat month_date = new SimpleDateFormat("HH:MM");
    String date = month_date.format(cal.getTime()).toString();
    System.out.println(date);
    return date;
}

答案 3 :(得分:2)

我这样做:

public class Factory {
    private static Factory factor;
    public static Factory getInstance() {
        return factor==null ? factor = new Factory() : factor;
    }
}

只是简单地

答案 4 :(得分:1)

一段时间后(我知道是花了2年的时间),我认为我有正确的答案。从字面上看,答案是:

但是,对我而言,这只是问题。这是唯一的问题吗?

。您现在所拥有的方式,getInstance的调用者将永远不会看到null。但是,如果Singleton有字段,则不能保证将这些字段正确初始化。

让我们慢慢来,因为示例很漂亮,恕我直言。您显示的代码仅执行一个{racy} volatile read

public class Factory {
    private Singleton instance;
    public Singleton getInstance() {
        Singleton res = instance;      // <-- volatile RACY read
        if (res == null) {
            synchronized (this) {
                res = instance;        // <-- volatile read under a lock, thus NOT racy
                if (res == null) {
                    res = new Singleton();
                    instance = res;
                }
            }
        }
        return res;
    }
}

通常,经典的“双重检查锁定”具有易失性的两个读取,例如:

public class SafeDCLFactory {

   private volatile Singleton instance;

   public Singleton get() {
     if (instance == null) {             // <-- RACY read 1 
       synchronized(this) {
         if (instance == null) {         // <-- non-racy read
            instance = new Singleton();
         }
       }
     }
     return instance;                    // <-- RACY read 2
   }
} 

因为这两个读物很活泼,没有易失,所以这种模式被打破了。您可以阅读我们如何打破here, for example

在您的情况下,有一个优化,它减少了对易失性字段的读取。在某些平台上,这很重要,afaik。

问题的另一部分更有趣。如果Singleton有一些我们需要设置的字段怎么办?

static class Singleton {
     
   //setter and getter also
   private Object obj;  
   
} 

还有一个工厂,Singleton volatile

static class Factory {

    private volatile Singleton instance;

    public Singleton get(Object obj) {
        if (instance == null) {
            synchronized (this) {
                if (instance == null) {
                    instance = new Singleton();
                    instance.setObj(obj);
                }
            }
        }
        return instance;
    }
}

我们的领域动荡不安,我们很安全,对吧?错误。 obj的分配在易失性写入之后 之后发生,因此无法保证。用简单的英语:this should help you a lot

解决此问题的正确方法是对已经构建的实例(完全构建)执行易失性写操作:

  if (instance == null) {
       Singleton local = new Singleton();
       local.setObj(obj);
       instance = local;
  }

答案 5 :(得分:0)

  

现在,第二个线程可以读取处于不一致状态的实例。

我很确定这是该代码中唯一的问题。我理解它的方式,一行

instance = res;
执行

,另一个线程可以读取instance并将其视为非空,从而跳过synchronized。这意味着这两个线程之间没有发生之前的关系,因为只有当两个线程在同一个对象上同步或访问相同的volatile字段时才存在这些关系。

其他答案已经与Safe Publication and Safe Initialization in Java相关联,后者提供了以下解决不安全出版物的方法:

  • 制作instance字段volatile。所有线程都必须读取相同的volatile变量,该变量建立happens-before relation

      

    对易失性字段(第8.3.1.4节)的写入发生在每次后续读取该字段之前。

  • 将单例包装到一个包装器中,该包装器将单例存储在final字段中。 final字段的规则没有像之前发生的关系那样正式指定,我能找到的最佳解释是final Field Semantics

      

    当构造函数完成时,对象被认为是完全初始化的。在该对象完全初始化之后只能看到对象引用的线程可以保证看到该对象的 final 字段的正确初始化值。

    (不是对最终字段的强调和限制,至少在理论上可能会看到其他字段处于不一致状态)

  • 确保单例本身仅包含最终字段。解释与上面的解释相同。

答案 6 :(得分:0)

问题中提到的代码的问题是reordering可能发生,并且线程可以获得单例类的部分构造的对象。

当我说reordering时,我指的是以下内容:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
                /* The above line executes the following steps:
                   1) memory allocation for Singleton class
                   2) constructor call ( it may have gone for some I/O like reading property file etc...
                   3) assignment ( look ahead shows it depends only on memory allocation which has already happened in 1st step. 
                   If compiler changes the order, it might assign the memory allocated to the instance variable. 
                   What may happen is half initialized object will be returned to a different thread )
                */
            }
        }
    }
    return instance;
} 

声明实例变量volatile可确保在上述3个步骤中建立happens-before/ordered关系:

  

在对该字段的每次后续读取之前发生对易失性字段(第8.3.1.4节)的写入。

来自维基百科的Double-checked locking

  

从J2SE 5.0开始,此问题已得到修复。 volatile关键字现在确保多个线程正确处理单例实例。 The "Double-Checked Locking is Broken" Declaration

中描述了这个新的习语
// Works with acquire/release semantics for volatile
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }

    // other functions and members...
}