Статьи

Ленивая оценка

Недавно я писал 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 миллиона вызовов, используя другое количество потоков.

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

2 типа

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

Код для этого блога доступен @ github

Ссылка: Ленивая оценка от нашего партнера JCG Ашкрит Шарма в блоге « Готовы ли вы» .