避免线程安全记忆供应商中的易失性读取

Avoiding volatile reads in thread-safe memoizing Supplier

我想创建给定 Supplier 的记忆版本,以便多个线程可以同时使用它,并保证原始供应商的 get() 最多被调用一次,并且所有线程看到相同的结果。双重检查锁定似乎很合适。

class CachingSupplier<T> implements Supplier<T> {
    private T result = null;

    private boolean initialized = false;

    private volatile Supplier<? extends T> delegate;

    CachingSupplier(Supplier<? extends T> delegate) {
        this.delegate = Objects.requireNonNull(delegate);
    }

    @Override
    public T get() {
        if (!this.initialized && this.delegate != null) {
            synchronized (this) {
                Supplier<? extends T> supplier = this.delegate;
                if (supplier != null) {
                    this.result = supplier.get();
                    this.initialized = true;
                    this.delegate = null;
                }
            }
        }
        return this.result;
    }
}

我的理解是在这种情况下 delegate 需要是 volatile 因为否则 synchronized 块中的代码可能会被重新排序:写入 delegate 可能发生在写入 result 之前,可能会在完全初始化之前将 result 暴露给其他线程。对吗?

因此,通常这需要在每次调用时在 synchronized 块之外对 delegate 进行易失性读取,每个竞争线程最多只进入一次 synchronized 块,而 result 未初始化,以后再也不会。

但是一旦 result 被初始化,是否也可以通过首先检查非易失性标志来避免在后续调用中对 delegate 的非同步易失性读取的成本(无论多么微不足道) initialized 短路?还是这与正常的双重检查锁定相比绝对没有给我带来任何好处?或者它是否以某种方式损害性能而不是帮助?还是真的坏了?

它已损坏,即它不是多线程安全的。 根据 JMM,只是 "seeing" 一个共享内存值(在您的示例中,一个 reader 线程可能会将 #initialized 视为 true),这不是一个发生前的关系,因此 reader 线程可以:

load initialized //evaluates true
load result //evaluates null

以上是允许的执行。

无法避免 "cost" 同步操作(例如,易失性写入的易失性读取),同时避免数据竞争(以及因此损坏的代码)。句号。

概念上的困难在于打破常识推断,即线程要将初始化视为 true -> 必须先 将 true 写入已初始化;虽然很难接受,但这个推论是不正确的

正如 Ben Manes 指出的那样,volatile 读取只是 x-86 上的普通负载

不要实施双重检查锁定,使用可以为您完成工作的现有工具:

class CachingSupplier<T> implements Supplier<T> {
    private final Supplier<? extends T> delegate;
    private final ConcurrentHashMap<Supplier<? extends T>,T> map=new ConcurrentHashMap<>();

    CachingSupplier(Supplier<? extends T> delegate) {
        this.delegate = Objects.requireNonNull(delegate);;
    }

    @Override
    public T get() {
        return map.computeIfAbsent(delegate, Supplier::get);
    }
}

请注意,在 将其发布到其他线程之前,简单地进行急切的首次评估并用常量返回的供应商替换供应商 比以往任何时候都更简单和足够。或者只是使用 volatile 变量并接受如果多个线程遇到尚未评估的供应商可能会有一些并发评估。


下面的实现仅供参考(学术)目的,强烈推荐上面更简单的实现。

您可以改用不可变对象的发布保证:

class CachingSupplier<T> implements Supplier<T> {
    private Supplier<? extends T> delegate;
    private boolean initialized;

    CachingSupplier(Supplier<? extends T> delegate) {
        Objects.requireNonNull(delegate);
        this.delegate = () -> {
            synchronized(this) {
                if(!initialized) {
                    T value = delegate.get();
                    this.delegate = () -> value;
                    initialized = true;
                    return value;
                }
                return this.delegate.get();
            }
        };
    }

    @Override
    public T get() {
        return this.delegate.get();
    }
}

在这里,initialized 是在 synchronized(this) 守卫下写入和读取的,但是在第一次评估时,delegate 被一个新的 Supplier 取代,Supplier 总是 returns 无需任何检查的评估值。

由于新供应商是不可变的,因此即使被从未执行过 synchronized 块的线程读取也是安全的。


正如 igaz 正确指出的那样,如果 CachingSupplier 实例本身未安全发布,则上面的 class 无法避免数据竞争。完全不受数据竞争影响的实现,即使发布不当,但在普通访问情况下仍然可以在没有内存障碍的情况下工作,甚至更复杂:

class CachingSupplier<T> implements Supplier<T> {
    private final List<Supplier<? extends T>> delegate;
    private boolean initialized;

    CachingSupplier(Supplier<? extends T> delegate) {
        Objects.requireNonNull(delegate);
        this.delegate = Arrays.asList(() -> {
            synchronized(this) {
                if(!initialized) {
                    T value = delegate.get();
                    setSupplier(() -> value);
                    initialized = true;
                    return value;
                }
                return getSupplier().get();
            }
        });
    }
    private void setSupplier(Supplier<? extends T> s) {
        delegate.set(0, s);
    }
    private Supplier<? extends T> getSupplier() {
        return delegate.get(0);
    }

    @Override
    public T get() {
        return getSupplier().get();
    }
}

我认为这更加强调了第一个解决方案的美感……