如何为对象引用构建延迟初始化线程安全包装器

时间:2018-08-15 00:31:45

标签: java concurrency thread-safety synchronized volatile

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

  • 一个构造函数,它使用逻辑来创建包装的类的实例。例如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

1 个答案:

答案 0 :(得分:1)

关于该问题的评论是正确的:如果您仅在value方法内访问synchronized,那么您也不必同时是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 Java Java Concurrency in Practice 中显示的一些示例。请注意,此代码检查两次,以检查result是否为null,一次在synchronized块外,一次在内部。这样做的好处是,如果值已经存在,则不需要同步。请注意,采用这种策略,value必须为volatile,因为它是在synchronized块之外访问的。