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

Nat*_*hes 14 java concurrency multithreading

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

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

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

Ido*_*lon 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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


Gra*_*ray 5

编辑:

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

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

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

我认为这在很大程度上取决于应用程序是否存在任何内存障碍 - 任何synchronized阻止或访问volatile字段.魔鬼肯定在这里的细节,并且执行延迟初始化的代码可以在一个具有一组代码的架构上工作,而不是在不同的线程模型中或者与很少同步的应用程序.


作为比较,这里是最后一个领域的好文章:

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

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

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


Pet*_*rey 5

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

  • 可以尝试不止一次设置该字段.
  • 如果单个线程看到不同的值,那就没关系.

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 然而,这会受到构造函数竞争条件问题的影响.您可以获取对导出到另一个线程的对象的引用,而不会完全初始化该对象. (2认同)