【问题标题】:how to build a lazy initializing thread safe wrapper for an object reference如何为对象引用构建延迟初始化线程安全包装器
【发布时间】:2018-08-15 00:31:45
【问题描述】:

我想实现一个包装类。该课程唯一面向公众的内容是:

  • 采用逻辑来创建包装类的实例的构造函数。比如Supplier<WrappedType>,也许吧。
  • 获取包装类实例的方法。

对其行为有以下规则:创建包装类的逻辑可能有副作用,并且只能调用一次。而且,显然,getter 方法实际上应该总是返回相同的包装类实例,这实际上应该是运行传递给构造函数的逻辑的结果。

我想我有这段代码可以做我想做的事,但我不确定如何测试它是否可以保证工作,或者是否有更好的方法。

package foo;

import java.util.function.Supplier;

public final class ConcurrentLazyContainer<A> {
    private Supplier<A> supplier;
    private A value;

    public ConcurrentLazyContainer(Supplier<A> supplier) {
        this.supplier = supplier;
        value = null;
    }

    public synchronized A get() {
        if (value == null) {
            value = supplier.get();
            supplier = null;
        }

        return value;
    }
}

仅使用synchronized 就可以让我一直得到我想要的吗?我的字段是否也需要是 volatile 的?

我编写了一个测试,该测试启动了调用同一个包装器的新线程,但在我看来,供应商并没有被多次调用,这很奇怪,因为我真的不明白为什么在这里不需要 volatile .

【问题讨论】:

  • 不,您不需要 volatile,因为同步会创建自己的 happens-before 边缘。
  • 我的想法是,happens-before 保证意味着只有一个线程在get() 中执行,但这并不能告诉我线程是在缓存supplier 还是value。但我也不太了解 Java 的内存系统,所以“嗯,东西可能会在给定线程的内存中本地缓存或其他东西”是我对 volatile 所做的全部了解。
  • 这根本不是发生之前的意思。 Happens-before 是防止对 volatile 进行重新排序和缓存的保证。除了互斥之外,同步也恰好提供了这种保证。所以value 是安全的。至于supplier,这取决于容器是published,这完全是另一个讨论。确保正确性的最简单方法是将其设为 final 并忘记清理。或设为volatile
  • 好的,因此该内存规范中的某处描述了发生之前,如果我的代码满足其要求,那么线程将不会出现看到错误值的缓存问题。供应商可以是最终的,这没问题。谢谢您的帮助!如果您将此作为答案提交,我会接受。

标签: java concurrency thread-safety synchronized volatile


【解决方案1】:

这个问题的cmets是对的:如果你只在synchronized方法中访问value,那么你也不需要它是volatile。但是,在某些情况下,您可以使用 double-checked locking 提高性能。

public final class Lazy<T> {

  private final Supplier<? extends T> initializer;
  private volatile T value;

  public Lazy(Supplier<? extends T> initializer) {
    this.initializer = initializer;
  }

  public T get() {
    T result = this.value;
    if (result == null) {
      synchronized (this) {
        result = this.value;
        if (result == null) {
          this.value = result = this.initializer.get();
        }
      }
    }
    return result;
  }

}

此代码基于 Effective JavaJava Concurrency in Practice 中显示的一些示例。请注意,此代码检查两次以查看result 是否为null,一次在synchronized 块之外,一次在内部。这样做的好处是,如果值已经存在,您将不需要同步。请注意,使用此策略时,value 必须是 volatile,因为它是在 synchronized 块之外访问的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-17
    • 2015-07-27
    • 1970-01-01
    • 2014-07-11
    • 2011-03-15
    • 1970-01-01
    相关资源
    最近更新 更多