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

时间:2017-12-30 06:59:04

标签: java multithreading concurrency thread-safety double-checked-locking

我想创建一个给定Supplier的memoized版本,这样多个线程可以同时使用它,保证原始供应商的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,可能在delegate完全初始化之前将synchronized暴露给其他线程。这是对的吗?

因此,通常这会导致在每次调用时synchronized块之外的result易失性读取,每个竞争线程最多只进入result块一次{{1没有初始化,然后再也没有。

但是一旦delegate被初始化,是否有可能通过首先检查非易失性标志{{}来避免后续调用中initialized的非同步易失性读取的成本,但是可以忽略不计。 1}}和短路?或者,除了正常的双重锁定之外,这对我来说绝对没有什或者它是否会以某种方式损害性能而不是它有帮助?还是它真的坏了?

2 个答案:

答案 0 :(得分:2)

它已损坏,即它不是多线程安全的。 据JMM说,只是&#34;看到&#34;共享内存值(在您的示例中,读者线程可能会将#initialized视为true),不是发生在之前的关系,因此读者线程可以:

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

以上是允许的执行。

没有办法避免&#34;成本&#34;同步动作(例如,易失性写入的易失性读取)并且同时避免数据竞争(以及因此破坏的代码)。完全停止。

概念上的困难在于打破常见推理,即线程看到初始化为真 - &gt;必须有一个之前的写入true来初始化;很难接受,推断是不正确的

正如Ben Manes指出的那样,易变性读取只是x-86

上的普通负载

答案 1 :(得分:2)

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

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取代,后者总是返回评估没有任何检查的价值。

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

正如igaz正确指出的那样,如果CachingSupplier实例本身未安全发布,则上述类不能免于数据竞争。即使在发布不正确但在普通访问案例中仍然没有内存障碍的情况下,完全不受数据竞争影响的实现更为复杂:

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();
    }
}

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