【问题标题】:Lazy initialization without synchronization or volatile keyword没有同步或 volatile 关键字的延迟初始化
【发布时间】:2012-06-15 13:45:22
【问题描述】:

前几天 Howard Lewis Ship 发表了一篇名为 "Things I Learned at Hacker Bed and Breakfast" 的博客文章,其中一个要点是:

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

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

【问题讨论】:

  • 不要害怕易失性读取。类初始化,即可修改的代码是唯一可移植的方式来做到这一点,没有易失性。面对允许重新排序写入的 CPU 架构,该声明是不正确的。在 x86 和 Sparc TSO 上,易失性读取是免费的,因此没有必要扮演黑客。

标签: java multithreading concurrency


【解决方案1】:

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

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

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

【讨论】:

【解决方案2】:

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

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

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

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


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

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

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

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

【讨论】:

    【解决方案3】:

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

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

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

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

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

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

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

    【讨论】:

    • 然而,这会受到构造函数竞争条件问题的影响。您可以获得对导出到另一个线程的对象的引用,而无需完全初始化对象。
    • @Gray 我对延迟初始化的理解是它总是根据需要进行初始化。应该不可能看到未初始化的值,但您可以尝试多次设置它。
    • 我看到@Peter 的问题是,如果没有同步,没有内存屏障,那么有可能在没有 full的情况下在内存缓存之间共享对象的一部分> 对象存储正在更新。如果在另一个进程上运行的线程 A 有它自己的第 1 页副本,但没有第 2 页副本,并且在每个页面中都有存储的对象被线程 B 延迟初始化,那么线程 A 可以获得部分初始化的对象。
    • 我想我在这里@Peter 有点毛骨悚然,但据我所知,问题出在缓存的内存页面上。如果线程 A 有它自己的页面 #1 副本,因为它在该页面中本地更改了某些内容,但没有它是页面 #2 的自己的副本。如果prop 跨越两个内存页面并且已经被线程 B 初始化,那么当线程 A 第一次以非同步方式访问 prop 时,它将把页面 #2 加载到它的内存中,但它不会刷新页面 #1 并且可能会得到初始化的prop 对象的一半。
    • @Gray 是正确的。在可以对写入进行重新排序的架构(Alpha/Itanium)上,可以“看到”一半初始化的Properties 对象——比如Entry[] tablenull。这是双重锁定不起作用的主要原因。始终使用易失性 - 读取只是便宜甚至免费......所以,如果底层架构足够“弱”,它不会分裂头发,为了看到一个正确初始化的对象,必须发出写写屏障(写-write 很便宜,不过)
    【解决方案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.");
        }
    
    }
    

    这个类不是线程安全的。

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

    【讨论】:

      猜你喜欢
      • 2019-06-03
      • 1970-01-01
      • 2012-08-06
      • 2020-05-10
      • 1970-01-01
      • 2011-11-17
      • 1970-01-01
      • 2014-11-02
      • 1970-01-01
      相关资源
      最近更新 更多