【问题标题】:Lazy initialization using functional programming使用函数式编程进行延迟初始化
【发布时间】:2020-03-19 23:55:39
【问题描述】:

让我们定义一个类,使得多个实例变量只能延迟初始化一次,就像这个类:

public class MyClass {
    private Object myLazyField;
    private Integer anotherLazyField;

    public Object getMyLazyField() {
        if (myLazyField == null) {
            synchronized (this) {
                if (myLazyField == null) {
                    myLazyField = new Object();
                }
            }
        }
        return myLazyField;
    }

    public Integer getAnotherLazyField() {
        if (anotherLazyField == null) {
            synchronized (this) {
                if (anotherLazyField == null) {
                    anotherLazyField = 10;
                }
            }
        }
        return anotherLazyField;
    }
}

代码相当丑陋,初始化结构重复。

我的问题是:如何简化延迟初始化,避免结构重复?

我试过了:

class Utility {
    public static <T> T init(Object object, T initialValue, Supplier<T> supplier) {
        synchronized (object) {
            if (initialValue == null) {
                return supplier.get();
            }
        }
        return initialValue;
    }
}

MyClass:

 public Object getMyLazyField() {
     if (myLazyField == null) {
         myLazyField = Utility.init(this, myLazyField, Object::new);
     }
     return myLazyField;
 }

更好,但我仍在寻找使用函数式编程的更好解决方案。

【问题讨论】:

  • 我不明白为什么你觉得第一个解决方案很丑。当然,重复的代码语句if (anotherLazyField == null) { 可以去掉。除此之外,它只有 3 行代码。您是否试图概括多个字段的延迟初始化?
  • 我不是 Java 注释处理方面的专家,但我相信您可以编写一个处理器,它会自动为带有适当注释的 getter 方法添加惰性。类似于不可变或自动值。
  • @dash-o 这绝对是个好主意。

标签: java functional-programming


【解决方案1】:

我有一个Lazy 课程,在我的个人项目中经常使用。一、用法:

public class MyClass {
    private final Lazy<Object> myLazyField = new Lazy<>(Object::new);
    private final Lazy<Integer> anotherLazyField = new Lazy<>(() -> 10);

    public Object getMyLazyField() { return myLazyField.get(); }
    public Integer getAnotherLazyField() { return anotherLazyField.get(); }
}

它使用您代码中的double-checked locking idiom,并进行了一项改进:字段标记为volatile

代码如下:

Lazy.java

import java.util.*;
import java.util.function.*;

/**
 * A lazy-loaded value that is only created when the value is required. If the
 * value is never used, possibly expensive initialization (either in time or in
 * memory usage) is avoided.
 * <p>
 * This class is thread-safe. Initialization will only ever be performed once,
 * and it is safe to call {@link #get()} simultaneously from different threads.
 * <p>
 * {@code Lazy} makes implementing singletons simple by handling the deferred
 * initialization logic for you. It can also be used as a simple form of caching
 * for expensive computations.
 */
public final class Lazy<T> {
    private volatile T value;
    private volatile Supplier<T> factory;
    private final Object lock = new Object();

    /** Create a lazy object that gets its value from the supplied factory. */
    public Lazy(Supplier<T> factory) {
        this.value = null;
        this.factory = factory;
    }

    /**
     * Create a lazy object that holds the given value. This constructor can be
     * used if you happen to have already computed the value.
     */
    public Lazy(T value) {
        this.value = value;
        this.factory = null;
    }

    /** Get the value. If this is the first call, the value is initialized. */
    public T get() {
        // The double-checked locking idiom is safe in Java when the tested variable is
        // volatile, which `factory` is.
        if (factory != null) {
            synchronized (lock) {
                if (factory != null) {
                    value = factory.get();
                    factory = null;
                }
            }
        }

        return value;
    }

    @Override
    public String toString() {
        return Objects.toString(get());
    }

    @Override
    public boolean equals(Object object) {
        return (object instanceof Lazy) && Objects.equals(get(), ((Lazy) object).get());
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(get());
    }
}

维基百科文章解释了为什么naïve double-checked locking is unsafe

直观地说,这个算法似乎是解决问题的有效方法。然而,这种技术有许多微妙的问题,通常应该避免。例如,考虑以下事件序列:

  1. 线程A注意到该值没有被初始化,因此它获得了锁并开始初始化该值。
  2. 由于某些编程语言的语义,允许编译器生成的代码在 A 完成初始化之前更新共享变量以指向部分构造的对象。例如,在 Java 中,如果对构造函数的调用已内联,则共享变量可能会在分配存储后但在内联构造函数初始化对象之前立即更新。
  3. 线程 B 注意到共享变量已被初始化(或看起来如此),并返回其值。因为线程 B 认为该值已经初始化,所以它不会获取锁。如果 B 在 B 看到 A 完成的所有初始化之前使用该对象(因为 A 尚未完成初始化它或因为对象中的某些初始化值尚未渗透到 B 使用的内存中 (cache coherence) ),程序可能会崩溃。

使用volatile 解决了上述问题并使其在Java 1.5+ 中使用安全。

【讨论】:

  • 感谢您的回答。我知道volatile 技巧(IntelliJ 抱怨在运行代码检查时缺少 volatile)。我认为这是个好主意。我唯一反对的是lock 对象:当多个线程正在执行代码时,可能会创建多个Lazy 实例并且锁定将无法正常工作(除非我出错了)。你能提供一个如何在我的代码中使用它的例子吗?
  • 我看到的问题是我必须像您一样将Lazy 实例声明为实例变量,我不能将它们用作直接在getter 中定义的方法变量。除此之外,我觉得一切都很好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-11-17
  • 2015-05-21
  • 1970-01-01
  • 2020-05-10
  • 1970-01-01
  • 2011-06-04
  • 1970-01-01
相关资源
最近更新 更多