Как реализовать поточную безопасную ленивую инициализацию?
Каковы некоторые рекомендуемые подходы для достижения поточной безопасной ленивой инициализации? Например,
// 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, как следует из названия, имеет следующие свойства:
- Объект-обертка, который оборачивает функцию F. В текущем контексте F - это функция / лямбда-выражение, которое содержит код инициализации / деинициализации.
Оболочка предоставляет метод execute, который ведет себя как:
- Вызывает функцию F при первом вызове execute и кэширует выходные данные F.
- Если вызов двух или более потоков выполняется одновременно, только один "входит", а остальные блокируются, пока не завершится тот, который "вошел".
- Для всех других / будущих вызовов execute он не вызывает 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'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;
}