Ленивая загрузка ссылки

Будучи впечатлен 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(), который функционирует как метод factory:

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

Я не уверен, что эта реализация является оптимальной, но она демонстрирует, что мне нужно.

Позже я нашел этот пример из T2 Framework, который выглядит более сложным.

Теперь мои вопросы:

  • Как можно улучшить код выше?
  • Как он сравнивается с примером T2 и какие преимущества в этом примере могут быть более сложными?
  • Существуют ли другие версии ленивой загрузки, которые я пропустил в моем поиске?

EDIT: Обновлена ​​моя реализация, чтобы использовать локальную переменную, предложенную @irreputable answer - пожалуйста, повысьте ее, если вы найдете приведенный выше пример полезно.

Ответ 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() завершен, убедитесь, что вы выполнили необходимую очистку.

(Обратите внимание, что если референт должен быть одиночным, тогда я бы использовал инициализацию по требованию владельца idiom.)

Ответ 3

Это хорошая старая двукратная блокировка идиомы. Вы должны добавить локальную переменную для производительности. В вашем случае у вас есть 2 изменчивых чтения на быстром пути (когда установлен референт). Проверьте http://en.wikipedia.org/wiki/Double-checked_locking