没有同步或易失性关键字的惰性初始化

2022-09-03 13:24:32

前几天,霍华德·刘易斯·希普(Howard Lewis Ship)发布了一篇名为“我在Hacker Bed and Breakfast学到的东西”的博客文章,其中一个要点是:

通过延迟初始化只分配一次的 Java 实例字段不必同步或易失性(只要您可以接受跨线程的争用条件以分配给该字段);这是来自Rich Hickey

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


答案 1

这种说法听起来有点晦涩难懂。但是,我猜HLS指的是当您懒惰地初始化实例字段并且不关心多个线程是否多次执行此初始化的情况。
作为一个例子,我可以指出类的方法:hashCode()String

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;
}

如您所见,对字段(保存计算的字符串哈希的缓存值)的访问未同步,并且该字段未声明为 。任何调用 method 的线程仍将接收相同的值,尽管字段可能由不同的线程多次写入。hashCodevolatilehashCode()hashCode

此技术的可用性有限。恕我直言,它主要用于示例中的情况:一个缓存的基元/不可变对象,该对象是从其他最终/不可变字段计算的,但它在构造函数中的计算是过度的。


答案 2

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

延迟初始化的线程可能会在多方面遭受同步问题的影响。例如,可以具有构造函数争用条件,其中类的引用已导出,但类本身未完全初始化。

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

我认为这在很大程度上取决于应用程序是否有任何内存障碍 - 任何块或对字段的访问。魔鬼当然在这里的细节中,执行延迟初始化的代码可能在具有一组代码的一个体系结构上工作正常,而不是在另一个线程模型或很少同步的应用程序上。synchronizedvolatile


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

http://www.javamex.com/tutorials/synchronization_final.shtml

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

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


推荐