Как реализовать поточную безопасную ленивую инициализацию?

Каковы некоторые рекомендуемые подходы для достижения поточной безопасной ленивой инициализации? Например,

// Not thread-safe
public Foo getInstance(){
    if(INSTANCE == null){
        INSTANCE = new Foo();
    }

    return INSTANCE;
}

13 ответов

Решение

Для синглетонов существует элегантное решение, делегирующее задачу в код JVM для статической инициализации.

public class Something {
    private Something() {
    }

    private static class LazyHolder {
            public static final Something INSTANCE = new Something();
    }

    public static Something getInstance() {
            return LazyHolder.INSTANCE;
    }
}

увидеть

http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom

и этот пост в блоге Crazy Bob Lee

http://blog.crazybob.org/2007/01/lazy-loading-singletons.html

Если вы используете Apache Commons Lang, вы можете использовать один из вариантов ConcurrentInitializer, например, LazyInitializer.

Пример:

lazyInitializer = new LazyInitializer<Foo>() {

        @Override
        protected Foo initialize() throws ConcurrentException {
            return new Foo();
        }
    };

Теперь вы можете безопасно получить Foo (инициализируется только один раз):

Foo instance = lazyInitializer.get();

Если вы используете гуаву от Google:

Supplier<Foo> fooSupplier = Suppliers.memoize(new Supplier<Foo>() {
    public Foo get() {
        return new Foo();
    }
});

Тогда позвони Foo f = fooSupplier.get();

От Suppliers.memoize Javadoc:

Возвращает поставщика, который кэширует экземпляр, полученный во время первого вызова get(), и возвращает это значение при последующих вызовах get(). Возвращенный поставщик является потокобезопасным. Метод get() делегата будет вызван не более одного раза. Если делегат - это экземпляр, созданный в результате более раннего вызова системы memoize, он возвращается напрямую.

Это можно сделать без блокировки с помощью AtomicReference в качестве держателя экземпляра:

// in class declaration
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;
   }
}

Основным недостатком здесь является то, что несколько потоков могут одновременно создавать два или более экземпляров. Foo объектов, и только один будет удачным для настройки, поэтому, если для создания экземпляра требуется ввод-вывод или другой общий ресурс, этот метод может не подойти.

С другой стороны, этот подход не блокируется и не требует ожидания: если один поток, который первым вошел в этот метод, застрянет, это не повлияет на выполнение других.

Самый простой способ - использовать статический класс внутреннего держателя:

public class Singleton {

    private Singleton() {
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
}
class Foo {
  private volatile Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      synchronized(this) {
        if (helper == null) {
          helper = new Helper();
        }
      }
    }
  return helper;
}

Это называется двойной проверкой! Проверьте это http://jeremymanson.blogspot.com/2008/05/double-checked-locking.html

Если вы используете lombok в своем проекте, вы можете использовать функцию, описанную здесь.

Вы просто создаете поле, комментируете его @Getter(lazy=true) и добавьте инициализацию, вот так:@Getter(lazy=true) private final Foo instance = new Foo();

Вам придется ссылаться на поле только с помощью getter (см. Примечания в документации по lombok), но в большинстве случаев это то, что нам нужно.

Вот еще один подход, основанный на семантике одноразового исполнителя.

Полное решение с кучей примеров использования можно найти на github ( https://github.com/ManasjyotiSharma/java_lazy_init). Вот суть этого:

Семантика One Time Executor, как следует из названия, имеет следующие свойства:

  1. Объект-обертка, который оборачивает функцию F. В текущем контексте F - это функция / лямбда-выражение, которое содержит код инициализации / деинициализации.
  2. Оболочка предоставляет метод execute, который ведет себя как:

    • Вызывает функцию F при первом вызове execute и кэширует выходные данные F.
    • Если вызов двух или более потоков выполняется одновременно, только один "входит", а остальные блокируются, пока не завершится тот, который "вошел".
    • Для всех других / будущих вызовов execute он не вызывает F, а просто возвращает ранее кэшированный вывод.
  3. Кэшированный вывод может быть безопасно доступен извне контекста инициализации.

Это может быть использовано как для инициализации, так и для неидемпотентной деинициализации.

import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

/**
 * When execute is called, it is guaranteed that the input function will be applied exactly once. 
 * Further it's also guaranteed that execute will return only when the input function was applied
 * by the calling thread or some other thread OR if the calling thread is interrupted.
 */

public class OneTimeExecutor<T, R> {  
  private final Function<T, R> function;
  private final AtomicBoolean preGuard;
  private final CountDownLatch postGuard;
  private final AtomicReference<R> value;

  public OneTimeExecutor(Function<T, R> function) {
    Objects.requireNonNull(function, "function cannot be null");
    this.function = function;
    this.preGuard = new AtomicBoolean(false);
    this.postGuard = new CountDownLatch(1);
    this.value = new AtomicReference<R>();
  }

  public R execute(T input) throws InterruptedException {
    if (preGuard.compareAndSet(false, true)) {
      try {
        value.set(function.apply(input));
      } finally {
        postGuard.countDown();
      }
    } else if (postGuard.getCount() != 0) {
      postGuard.await();
    }
    return value();
  }

  public boolean executed() {
    return (preGuard.get() && postGuard.getCount() == 0);
  }

  public R value() {
    return value.get();
  }

}  

Вот пример использования:

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

/*
 * For the sake of this example, assume that creating a PrintWriter is a costly operation and we'd want to lazily initialize it.
 * Further assume that the cleanup/close implementation is non-idempotent. In other words, just like initialization, the 
 * de-initialization should also happen once and only once.
 */
public class NonSingletonSampleB {
  private final OneTimeExecutor<File, PrintWriter> initializer = new OneTimeExecutor<>(
    (File configFile) -> {
      try { 
        FileOutputStream fos = new FileOutputStream(configFile);
        OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
        BufferedWriter bw = new BufferedWriter(osw);
        PrintWriter pw = new PrintWriter(bw);
        return pw;
      } catch (IOException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
      }
    }
  );  

  private final OneTimeExecutor<Void, Void> deinitializer = new OneTimeExecutor<>(
    (Void v) -> {
      if (initializer.executed() && null != initializer.value()) {
        initializer.value().close();
      }
      return null;
    }
  );  

  private final File file;

  public NonSingletonSampleB(File file) {
    this.file = file;
  }

  public void doSomething() throws Exception {
    // Create one-and-only-one instance of PrintWriter only when someone calls doSomething().  
    PrintWriter pw = initializer.execute(file);

    // Application logic goes here, say write something to the file using the PrintWriter.
  }

  public void close() throws Exception {
    // non-idempotent close, the de-initialization lambda is invoked only once. 
    deinitializer.execute(null);
  }

}

Еще несколько примеров (например, одноэлементная инициализация, которая требует, чтобы некоторые данные были доступны только во время выполнения и, следовательно, не могли создать его экземпляр в статическом блоке), пожалуйста, обратитесь к ссылке на github, указанной выше.

Думая о ленивой инициализации, я ожидаю получить "почти настоящий" объект, который просто украшает еще не инициализированный объект.

Когда первый метод вызывается, экземпляр в украшенном интерфейсе будет инициализирован.

* Из-за использования прокси инициированный объект должен реализовывать переданный интерфейс.

* Отличие от других решений заключается в инкапсуляции инициации от использования. Вы начинаете работать непосредственно с DataSource как будто это было инициализировано. Он будет инициализирован при вызове первого метода.

Использование:

DataSource ds = LazyLoadDecorator.create(dsSupplier, DataSource.class)

За кулисами:

public class LazyLoadDecorator<T> implements InvocationHandler {

    private final Object syncLock = new Object();
    protected volatile T inner;
    private Supplier<T> supplier;

    private LazyLoadDecorator(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (inner == null) {
            synchronized (syncLock) {
                if (inner == null) {
                    inner = load();
                }
            }
        }
        return method.invoke(inner, args);
    }

    protected T load() {
        return supplier.get();
    }

    @SuppressWarnings("unchecked")
    public static <T> T create(Supplier<T> factory, Class<T> clazz) {
        return (T) Proxy.newProxyInstance(LazyLoadDecorator.class.getClassLoader(),
                new Class[] {clazz},
                new LazyLoadDecorator<>(factory));
    }
}

В зависимости от того, что вы пытаетесь достичь:

Если вы хотите, чтобы все потоки совместно использовали один и тот же экземпляр, вы можете синхронизировать метод. Этого будет достаточно

Если вы хотите сделать отдельный INSTANCE для каждого потока, вы должны использовать java.lang.ThreadLocal

Основываясь на этом ответе @Alexsalauyou , я подумал, можно ли реализовать решение, которое не вызывает несколько экземпляров.

В принципе, мое решение может быть немного медленнее (очень-очень немного), но оно определенно дружелюбнее к процессору и сборщику мусора.

Идея состоит в том, что вы должны сначала использовать контейнер, который может содержать значение «int» ПЛЮС общий, который вы хотите использовать.

      static class Container<T> {
   final int i; 
   final T val; 
   //constructor here
}

Пусть поля этого контейнера будут окончательными для целей параллелизма.

В LazyInit<T>класс должен иметь AtomicReference этого контейнера.

      AtomicReference<Container<T>> ref;

LazyInit должен определять фазовые процессы как закрытые статические целочисленные константы:

      private static final int NULL_PHASE = -1, CREATING_PHASE = 0, CREATED = 1;

private final Container<T> NULL = new Container<>(NULL_PHASE, null),
CREATING = new Container<>(CREATING_PHASE, null);

AtomicReference должен быть инициализирован как NULL:

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

Наконец, метод get() будет выглядеть так:

      @Override
public T get() {
    Container<T> prev;
    do {
        if (ref.compareAndSet(NULL, CREATING)) {
            T res = builder.get();
            ref.set(new Container<>(CREATED, res));
            return res;
        } else {
            prev = ref.get();
            if (prev.i == CREATED) return prev.value;
        }
    } while (prev.i < CREATED);
    return prev.value;
}

С Java 8 мы можем добиться ленивой инициализации с безопасностью потоков. Если у нас есть класс Holder и ему нужны тяжелые ресурсы, мы можем лениво загрузить тяжелый ресурс, как это.

public class Holder {
    private Supplier<Heavy> heavy = () -> createAndCacheHeavy();

    private synchronized Heavy createAndCacheHeavy() {

        class HeavyFactory implements Supplier<Heavy> {
            private final Heavy heavyInstance = new Heavy();

            @Override
            public Heavy get() {
                return heavyInstance;
            }
        }
        if (!HeavyFactory.class.isInstance(heavy)) {
            heavy = new HeavyFactory();
        }
        return heavy.get();
    }

    public Heavy getHeavy() {
        return heavy.get();
    }
}

public class Heavy {
    public Heavy() {
        System.out.println("creating heavy");
    }
}

Поместите код в synchronized блок с некоторым подходящим замком. Существуют и другие высокоспециализированные методы, но я бы рекомендовал избегать их, если в этом нет крайней необходимости.

Также вы использовали случай SHOUTY, который имеет тенденцию указывать static но метод экземпляра. Если он действительно статичен, я предлагаю вам убедиться, что он никоим образом не изменяем. Если создание статического неизменяемого просто дорого, то загрузка классов в любом случае ленива. Возможно, вы захотите переместить его в другой (возможно, вложенный) класс, чтобы отложить создание до самого последнего возможного момента.

Попробуйте определить метод, который получает экземпляр как синхронизированный:

public synchronized Foo getInstance(){
   if(INSTANCE == null){
    INSTANCE = new Foo();
  }

  return INSTANCE;
 }

Или используйте переменную:

private static final String LOCK = "LOCK";
public synchronized Foo getInstance(){
  synchronized(LOCK){
     if(INSTANCE == null){
       INSTANCE = new Foo();
     }
  }
  return INSTANCE;
 }
Другие вопросы по тегам