Подтвердить что ты не робот

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

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

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

    return INSTANCE;
}
4b9b3361

Ответ 1

Для синглтонов есть элегантное решение, делегируя задачу 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

Ответ 2

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

Пример:

lazyInitializer = new LazyInitializer<Foo>() {

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

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

Foo instance = lazyInitializer.get();

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

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

Затем назовите его Foo f = fooSupplier.get();

From Suppliers.memoize javadoc:

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

Ответ 3

Это можно сделать без блокировки, используя 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, и только один из них будет удачным для настройки, поэтому, если для создания экземпляра требуется ввод-вывод или другой общий ресурс, этот метод может оказаться неприемлемым.

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

Ответ 4

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

public class Singleton {

    private Singleton() {
    }

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

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

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

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

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

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

Ответ 8

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

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

"Один раз Исполнитель", как следует из названия, имеет свойства:

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

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

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

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 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, упомянутой выше.

Ответ 9

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

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

Ответ 10

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

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

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

Ответ 11

С 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");
    }
}

Ответ 12

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

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