【问题标题】:Lazy loading reference implementation延迟加载参考实现
【发布时间】:2011-09-23 04:57:31
【问题描述】:

对 Guava 的 computing map feature 印象深刻,我正在寻找一种“计算参考”——一种延迟加载参考实现,它与 Guava 的易用性并行,我的意思是它可以处理所有锁定、加载和异常处理在底层,只暴露了一个get() 方法。

经过短暂的搜索后,我很快推出了自己的概念证明:

public abstract class ComputingRef<T> implements Callable<T> {

   private volatile T referent = null;
   private Lock lock = new ReentrantLock();

   public T get() {
      T temp = referent;
      if (temp == null) {
         lock.lock();
         try {
            temp = referent;
            if (temp == null) {
               try {
                  referent = temp = call();
               }
               catch (Exception e) {
                  if (e instanceof RuntimeException) {
                     throw (RuntimeException)e;
                  }
                  else {
                     throw new RuntimeException(e);
                  }
               }
            }
         }
         finally {
            lock.unlock();
         }
      }
      return temp;
   }
}

这个ComputingRef 可以匿名扩展以实现call(),它的作用是工厂方法:

ComputingRef<MyObject> lazySingletonRef = new ComputingRef<MyObject>() {
   @Override
   public MyObject call() {
      //fetch MyObject from database and return
   }
};

我不满意这个实现是最佳的,但它证明了我所追求的。

后来找到this example from the T2 Framework,貌似比较复杂。

现在我的问题是:

  • 如何改进我上面的代码?
  • 它与 T2 示例相比如何,该示例的复杂性更高提供了哪些优势?
  • 我在搜索中遗漏了延迟加载引用的其他实现吗?

编辑:按照@irreputable's answer 的建议更新了我的实现以使用局部变量 - 如果您觉得上述示例有用,请点赞。

【问题讨论】:

  • 如果您使用的是 Java 5 及更高版本,为什么不直接使用 AtomicReference
  • @AlistairIsrael - 我可能弄错了,但AtomicReference 本身似乎不支持延迟加载。
  • @AlistairIsrael - 或者你的意思是我应该在我的代码示例中使用它而不是 volatile
  • 是的,让您的ComputingMap 使用内部AtomicReference,而不是使用锁定并自己实施双重检查锁定。

标签: java reference lazy-loading guava


【解决方案1】:

参见Suppliers.memoize(Supplier) 懒惰地初始化一个值。

【讨论】:

  • +1 啊,完全错过了这个。我注意到MemoizingSupplier 使用了一个 volatile 布尔值initialized,而不是使用 volatile 值并检查它是否为空。这与典型的双重检查锁定习语相比如何?
  • 差不多,但是供应商可能返回null,所以它不能用来表示初始化。
  • 使用标准 Java 8 库是否可以实现相同的目标?
【解决方案2】:

这是古老的双重检查锁定习语。您应该为性能添加一个局部变量。在您的 impl 中,您在快速路径中有 2 个易失性读取(当设置了引用时)。检查http://en.wikipedia.org/wiki/Double-checked_locking

【讨论】:

  • +1 优秀的文章,谢谢。我会更新我的问题并引用你的答案。
【解决方案3】:

无论如何,我会这样做(然后我会担心以后的性能):

public abstract class ComputingRef<T> implements Callable<T> {

    private final AtomicReference<T> ref = new AtomicReference<T>();

    public T get() {
        if (ref.get() == null) {
            try {
                final T newValue = call();
                if (ref.compareAndSet(null, newValue)) {
                    return newValue;
                }
            } catch (final Exception e) {
                if (e instanceof RuntimeException) {
                    throw (RuntimeException) e;
                } else {
                    throw new RuntimeException(e);
                }
            }

        }
        return ref.get();
    }
}

这种方法的唯一“障碍”是存在可能导致引用对象的多个实例化的竞争条件(尤其是如果ComputingRef 在所有命中get() 的大量线程之间共享同时)。如果实例化引用类非常昂贵,或者您想不惜一切代价避免多次实例化,那么我也会使用您的双重检查锁定。

您还必须确保引用对象在其自身之后进行清理。否则,如果compareAndSet() 失败,请确保执行任何必要的清理。

(请注意,如果引用对象需要是单例,那么我会改用initialization on demand holder idiom。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-25
    相关资源
    最近更新 更多