懒加载参考实现

12

受到Guava的计算地图功能的印象,我正在寻找一种类似于"计算参考"的东西——一种懒加载的参考实现,与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
   }
};

我不满意这个实现是否最优,但它演示了我的目的。

后来我发现T2 Framework中的这个例子,它似乎更加复杂。

现在我的问题是:

  • 如何改进我上面的代码?
  • 与T2示例相比,它如何,并且该示例的更高复杂性提供了什么优势?
  • 在我的搜索中是否错过了其他懒加载引用的实现?

编辑:根据@irreputable的答案建议更新了我的实现-如果您发现上面的示例有用,请点赞。


如果您使用的是Java 5及以上版本,为什么不直接使用AtomicReference呢? - Alistair A. Israel
1
@AlistairIsrael - 我可能错了,但是 AtomicReference 似乎不能自己支持延迟加载。 - Paul Bellora
@AlistairIsrael - 或者你的意思是我应该在我的代码示例中使用它来代替“volatile”? - Paul Bellora
是的,让你的“ComputingMap”使用内部的“AtomicReference”,而不是拥有锁并自己实现双重检查锁定。 - Alistair A. Israel
3个回答

26

啊,完全错过了这个。我注意到MemoizingSupplier使用了一个易失性布尔值initialized,而不是将值设为易失性并检查其是否为空。这与典型的双重检查锁定惯用法相比如何? - Paul Bellora
1
基本上是一样的,但供应商可能会返回null,因此不能用它来表示初始化。 - Ben Manes
使用标准的Java 8库是否可以实现相同的功能? - Askar Kalykov
还有这个LazyReference的实现,它处理了初始化过程中的异常,尽管它现在已经有点老了:https://docs.atlassian.com/atlassian-util-concurrent/0.0.7/apidocs/index.html?com/atlassian/util/concurrent/LazyReference.html有人有更好的处理初始化过程中异常的实现吗? - Chris Mountford
还有这个LazyReference的实现,它处理初始化过程中的异常,尽管现在已经有点老了:https://docs.atlassian.com/atlassian-util-concurrent/0.0.7/apidocs/index.html?com/atlassian/util/concurrent/LazyReference.html有人有更好的处理初始化过程中异常的实现吗? - undefined

2
无论如何,这是我会这样做的(然后我会担心性能问题):
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()失败,请确保执行任何必要的清理。

(请注意,如果引用需要是单例,则我将使用按需初始化占位符习惯用法。)


2

+1 很棒的文章,谢谢。我会更新我的问题并引用你的答案。 - Paul Bellora

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接