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

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

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

    return INSTANCE;
}

11 ответов


для синглетов существует элегантное решение путем делегирования задачи коду 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

и это сообщение в блоге сумасшедшего Боба Ли

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();

От Поставщиков.Мемоизация javadoc:

возвращает поставщика, который кэширует экземпляр, полученный во время первого вызова get (), и возвращает это значение при последующих вызовах get(). Возвращенный поставщик потокобезопасным. Делегата метод GET() будет вызван в самый раз. Если delegate-это экземпляр, созданный предыдущим вызовом 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 объекты, и только один будет счастлив быть настроен, поэтому, если для создания экземпляра требуется ввод-вывод или другой общий ресурс, этот метод может не подходить.

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


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

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


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

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

вам придется ссылаться на поле только с геттером (см. Примечания в Ломбоке docs), но в большинстве случаев это то, что нам нужно.


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

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

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

* отличие от других решений заключается в инкапсуляции инициации от использования. Вы начинаете работа непосредственно с 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));
    }
}

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

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

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

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

    • вызывает функцию F при первом вызове execute и кэширует вывод F.
    • если вызов 2 или более потоков выполняется одновременно, только один "входит", а другие блокируются до тех пор, пока не будет выполнен тот, который "вошел".
    • для всех других / будущих вызовов 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);
  }

}

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


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

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


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

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

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


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

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;
 }