Java 没有同步或volatile关键字的延迟初始化

Java 没有同步或volatile关键字的延迟初始化,java,multithreading,concurrency,Java,Multithreading,Concurrency,前几天,霍华德·刘易斯·希普(Howard Lewis Ship)在博客上发表了一篇名为“要点之一是: 一个Java实例字段,通过lazy只分配一次 初始化不必是同步的或易失的(只要 因为您可以接受跨线程的争用条件以分配给 字段);这是Rich Hickey写的 从表面上看,这似乎与关于跨线程内存更改可见性的公认观点不符,如果Java并发性实践手册或Java语言规范中包含了这一点,那么我就错过了。但这是HLS在Brian Goetz出席的一次活动中从Rich Hickey那里得到的东西,所以看起

前几天,霍华德·刘易斯·希普(Howard Lewis Ship)在博客上发表了一篇名为“要点之一是:

一个Java实例字段,通过lazy只分配一次 初始化不必是同步的或易失的(只要 因为您可以接受跨线程的争用条件以分配给 字段);这是Rich Hickey写的


从表面上看,这似乎与关于跨线程内存更改可见性的公认观点不符,如果Java并发性实践手册或Java语言规范中包含了这一点,那么我就错过了。但这是HLS在Brian Goetz出席的一次活动中从Rich Hickey那里得到的东西,所以看起来肯定有什么东西。有人能解释一下这句话背后的逻辑吗

Hrm。当我读到这篇文章时,它在技术上是不正确的,但在实践中是可以的,有一些警告。只有最终字段可以安全地初始化一次并在多个线程中访问,而无需同步

延迟初始化线程可能会以多种方式遇到同步问题。例如,您可以具有构造函数竞争条件,其中类的引用已导出,而类本身未完全初始化

我认为这在很大程度上取决于是否有一个基本场或一个对象。可以多次初始化的基本字段,如果您不介意由多个线程进行初始化,则可以正常工作。但是,以这种方式进行的
HashMap
样式初始化可能会有问题。即使某些体系结构上的
long
值也可能在多个操作中存储不同的字,因此可能会导出值的一半,尽管我怀疑
long
永远不会跨越内存页,因此它永远不会发生

我认为这在很大程度上取决于应用程序是否有任何内存障碍——任何
同步
块或对
易失性
字段的访问。这里的细节无疑是个魔鬼,执行延迟初始化的代码可以在一个架构上用一组代码很好地工作,而不是在不同的线程模型中,或者在一个很少同步的应用程序中工作


这里有一篇关于最终字段的好文章作为比较:

在Java5中,final关键字的一个特殊用法是并发系统中一个非常重要且经常被忽略的武器。本质上,final可以用来确保在构造对象时,访问该对象的另一个线程不会看到该对象处于部分构造状态,否则可能会发生这种情况。这是因为当用作对象变量的属性时,final具有以下重要特征作为其定义的一部分:


现在,即使该字段标记为final,如果它是一个类,也可以修改该类中的字段。这是一个不同的问题,您仍然必须对此进行同步。

这在某些情况下可以正常工作

  • 可以尝试多次设置场地
  • 如果单个线程看到不同的值,则没有问题
通常,当您创建一个未更改的对象时(例如从磁盘加载属性),短时间内拥有多个副本不是问题

private static Properties prop = null;

public static Properties getProperties() {
    if (prop == null) {
        prop = new Properties();
        try {
            prop.load(new FileReader("my.properties"));
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }
    return prop;
}
从短期来看,这不如使用锁定,但从长期来看,它可能更有效。(虽然属性本身有一个锁,但您可以理解;)

嗯,这不是一个在所有情况下都有效的解决方案


也许关键是,在某些情况下,您可以使用更轻松的内存一致性技术。

我认为这种说法是不正确的。另一个线程可以看到部分初始化的对象,因此即使构造函数尚未完成运行,该引用也可以对另一个线程可见。这将在Java并发性实践第3.5.1节:

public class Holder {

    private int n;

    public Holder (int n ) { this.n = n; }

    public void assertSanity() {
        if (n != n)
            throw new AssertionError("This statement is false.");
    }

}
这个类不是线程安全的


如果visible对象是不可变的,那么我认为您是可以的,因为final字段的语义意味着您在其构造函数完成运行之前不会看到它们(第3.5.2节)。

这句话听起来有点神秘。但是,我想HLS指的是当您懒散地初始化一个实例字段时的情况,而不关心多个线程是否多次执行此初始化。
例如,我可以指出
String
类的
hashCode()
方法:

private int hashCode;

public int hashCode() {
    int hash = hashCode;
    if (hash == 0) {
        if (count == 0) {
            return 0;
        }
        final int end = count + offset;
        final char[] chars = value;
        for (int i = offset; i < end; ++i) {
            hash = 31*hash + chars[i];
        }
        hashCode = hash;
    }
    return hash;
}
private int hashCode;
公共int hashCode(){
int hash=hashCode;
如果(哈希==0){
如果(计数=0){
返回0;
}
最终整数结束=计数+偏移量;
最终字符[]字符=值;
对于(int i=偏移;i<结束;++i){
散列=31*散列+字符[i];
}
hashCode=hash;
}
返回散列;
}
如您所见,对
hashCode
字段(保存计算字符串哈希的缓存值)的访问未同步,并且该字段未声明为
volatile
。任何调用
hashCode()
方法的线程仍然会收到相同的值,尽管不同的线程可能会多次写入
hashCode
字段


这种技术的可用性有限。IMHO它主要适用于示例中的情况:缓存的原语/不可变对象是从其他final/immutable字段中计算出来的,但其在构造函数中的计算是一种过度杀伤力。

但这会受到构造函数竞争条件问题的影响。您可以获得导出到另一个线程的对象引用,而不必完全初始化该对象。@格雷我对延迟初始化的理解是,它总是根据需要进行初始化。不应该看到一个单一化的值,但是你可以尝试不止一次地设置它。我看到的问题是“彼得”,如果没有同步,没有内存障碍,那么就有PAR的可能性。