Безликая и безжизненная поточно-надежная инициализация

Для выполнения незапираемой и ожидающей ленивой инициализации я делаю следующее:

private AtomicReference<Foo> instance = new AtomicReference<>(null);  

public Foo getInstance() {
   Foo foo = instance.get();
   if (foo == null) {
       foo = new Foo();                       // create and initialize actual instance
       if (instance.compareAndSet(null, foo)) // CAS succeeded
           return foo;
       else                                   // CAS failed: other thread set an object 
           return instance.get();             
   } else {
       return foo;
   }
}

и он работает очень хорошо, за исключением одного: если два потока видят экземпляр null, они оба создают новый объект, и только одному повезет установить его с помощью операции CAS, что приводит к расходованию ресурсов.

Кто-нибудь предлагает другой незаметный ленивый шаблон инициализации, который уменьшает вероятность создания двух дорогостоящих объектов двумя параллельными потоками?

Ответы

Ответ 1

Если вам нужна истинная свобода блокировки, вам придется немного покрутиться. У вас могут быть права на создание нитей "win", но остальные должны вращаться, пока они не будут готовы.

private AtomicBoolean canWrite = new AtomicBoolean(false);  
private volatile Foo foo; 
public Foo getInstance() {
   while (foo == null) {
       if(canWrite.compareAndSet(false, true)){
           foo = new Foo();
       }
   }
   return foo;
}

У этого, очевидно, есть проблемы с занятым вращением (вы можете спать или уступать там), но я, вероятно, по-прежнему рекомендую Инициализацию по требованию.

Ответ 2

Я думаю, вам нужно иметь некоторую синхронизацию для самого создания объекта. Я бы сделал:

// The atomic reference itself must be final!
private final AtomicReference<Foo> instance = new AtomicReference<>(null);
public Foo getInstance() {
  Foo foo = instance.get();
  if (foo == null) {
    synchronized(instance) {
      // You need to double check here
      // in case another thread initialized foo
      Foo foo = instance.get();
      if (foo == null) {
        foo = new Foo(); // actual initialization
        instance.set(foo);
      }
    }
  }
  return foo;
}

Это очень распространенная картина, особенно для ленивых одиночек. Двойная проверка блокировки минимизирует количество выполняемых synchronized блоков.

Ответ 3

Я бы, вероятно, пошел с ленивым стилем Singleton:

private Foo() {/* Do your heavy stuff */}

private static class CONTAINER {
 private static final Foo INSTANCE = new Foo();
}

public static Foo getInstance() {
 return CONTAINER.INSTANCE;
}

На самом деле, я не вижу причин использовать поле члена AtomicReference для себя.

Ответ 4

Как насчет использования другой volatile переменной для блокировки? Вы можете сделать двойной замок с новой переменной?

Ответ 5

Я не уверен, что конечный результат должен быть ориентирован на производительность или нет, если да ниже - это не решение. можете ли вы проверить дважды, например, и после первой проверки вызова метода thread.sleep для случайных секунд mili менее 100 миллисекунд.

private AtomicBoolean canWrite = new AtomicBoolean(false);  
private volatile Foo foo; 
public Foo getInstance() {
   if(foo==null){
          Thread.Sleep(getRandomLong(50)) // you need to write method for it
         if(foo==null){
            foo = new Foo();
      }
   }
   return foo;
}