Недавно я писал log4j appender и хотел использовать в нем logger для регистрации некоторых диагностических подробностей во время создания пользовательского appender, но инициализация log4j завершается только после создания экземпляра appender, поэтому сообщения, зарегистрированные на этом этапе, игнорируются.
Я почувствовал необходимость ленивой инициализации в пользовательском приложении и начал искать варианты. В этом блоге я поделюсь вещами, которые я попробовал.
Одна вещь, которая пришла мне в голову, была синглтонным подходом, но теперь известен тот факт, что синглтон вызывает проблемы с тестированием и делает невозможным его расширение, поэтому подход смешивания параллелизма и построения объекта не так уж хорош.
Если требуется синглтон, лучше использовать платформу Dependency Injection, а не портить код приложения. Вернемся к ленивой инициализации / eval.
В некоторых языках программирования, таких как scala / swift и т. Д., Есть поддержка lazy, поэтому для этого не требуется никакого специального кода, но в Java-пространстве нам все равно приходится писать потокобезопасный код, чтобы сделать его правильным.
Давайте посмотрим на некоторые варианты, которые мы имеем в Java и какой тип производительности мы получаем.
— Перебор с использованием Синхронизированного
Это самый простой и неэффективный подход, который использует Scala. Scala один доступен @ ScalaLazy.java
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public class SingleLock<V> implements Lazy<V> { private Callable<V> codeBlock; private V value; public SingleLock(Callable<V> codeBlock) { this.codeBlock = codeBlock; } @Override public synchronized V get() { if (value == null) { setValue(); } return value; } private void setValue() { try { value = codeBlock.call(); } catch (Exception e) { throw new RuntimeException(e); } }} |
— Двойной замок
Это немного сложно написать и дает хорошую производительность.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class DoubleLock<V> implements Lazy<V> { private Callable<V> codeBlock; private V value; private volatile boolean loaded; public DoubleLock(Callable<V> codeBlock) { this.codeBlock = codeBlock; } @Override public V get() { if (!loaded) { synchronized (this) { if (!loaded) { setValue(); loaded = true; } } } return value; } private void setValue() { try { value = codeBlock.call(); } catch (Exception e) { throw new RuntimeException(e); } }} |
— Используя задачу Future
Этот подход прост в написании и дает хорошую производительность.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class LazyFutureTask<V> implements Lazy<V> { private final FutureTask<V> futureTask; public LazyFutureTask(Callable<V> codeBlock) { this.futureTask = new FutureTask<>(codeBlock); } @Override public V get() { futureTask.run(); return getValue(); } private V getValue() { try { return futureTask.get(); } catch (Exception e) { throw new RuntimeException(e); } }} |
Подход с двойным замком дает лучшую производительность, а грубая сила — худшая Я сделал быстрый тест для 1 миллиона вызовов, используя другое количество потоков.
|
| Производительность одиночной блокировки очень плохая, давайте посмотрим на число, сняв одиночную блокировку, чтобы увидеть, как выполнялась двойная блокировка и будущая задача.
Эти тесты выполняются очень быстро, но подробные номера тестов должны быть близки. Код для этого блога доступен @ github |
| Ссылка: | Ленивая оценка от нашего партнера JCG Ашкрит Шарма в блоге « Готовы ли вы» . |

