15

前几天 Howard Lewis Ship 发表了一篇名为“我在 Hacker Bed and Breakfast 学到的东西”的博客文章,其中一个要点是:

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

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

4

4 回答 4

9

这句话听起来有点神秘。但是,我猜 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;
}

如您所见,对hashCode字段(保存计算的 String 哈希的缓存值)的访问未同步,并且该字段未声明为volatile. 任何调用hashCode()方法的线程仍然会收到相同的值,尽管hashCode字段可能被不同的线程多次写入。

这种技术的可用性有限。恕我直言,它主要用于示例中的情况:一个缓存的原始/不可变对象,它是从其他最终/不可变字段计算出来的,但是它在构造函数中的计算是多余的。

于 2012-06-15T13:57:18.780 回答
6

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

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

我认为这在很大程度上取决于您是否拥有原始字段或对象。在您不介意多个线程进行初始化的情况下,可以多次初始化的原始字段可以正常工作。然而HashMap,以这种方式初始化样式可能会出现问题。甚至long某些架构上的值可能会在多个操作中存储不同的单词,因此可能会导出一半的值,尽管我怀疑 along永远不会跨越内存页面,因此它永远不会发生。

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


这是一个关于 final 字段的好文章作为比较:

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

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

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

于 2012-06-15T13:47:13.310 回答
4

这在某些条件下工作正常。

  • 可以尝试多次设置字段。
  • 如果各个线程看到不同的值,那也没关系。

通常,当您创建未更改的对象时,例如从磁盘加载属性,在短时间内拥有多个副本不是问题。

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

在短期内,这比使用锁定效率低,但从长远来看,它可能更有效。(虽然属性有它自己的锁,但你明白了;)

恕我直言,它不是适用于所有情况的解决方案。

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

于 2012-06-15T13:49:13.870 回答
4

我认为这种说法是不真实的。另一个线程可以看到一个部分初始化的对象,因此即使构造函数尚未完成运行,另一个线程也可以看到该引用。这在 Java Concurrency in Practice 中有所介绍,第 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.");
    }

}

这个类不是线程安全的。

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

于 2012-06-15T13:51:49.843 回答