我的 ExpirableLazyObject 不是线程安全的吗?有竞争条件吗?

Is my ExpirableLazyObject not thread safe? Is there a race condition?

前段时间,我创建了一个 Java class 将惰性初始化封装到 LazyObect 中。它保持并发和线程安全,还提供了一个 reset() 方法来清除值。

public final class LazyObject<T> {

    private volatile T value;
    private volatile boolean updated;
    private final Supplier<T> supplier;

    private LazyObject(Supplier<T> supplier) {
        this.supplier = supplier;
    }
    public T get() {
        if (!updated) {
            synchronized(this) {
                if (!updated) {
                    value = supplier.get();
                    updated = true;
                }
            }
        }
        return value;
    }
    public void reset() {
        if (updated) {
            synchronized(this) {
                if (updated) {
                    updated = false;
                    value = null;
                }
            }
        }
    }
    public static <B> LazyObject<B> forSupplier(Supplier<B> supplier) {
        return new LazyObject<B>(supplier);
    }
}

我想尝试将这个概念更进一步。如果 LazyObject 持有一个仅在短时间内使用的非常大的对象(如内存密集型哈希图),我希望在一段时间不使用后销毁该值。如果稍后再次使用它,它将再次初始化并重新安排另一个销毁周期。

所以我创建了 ExpirableLazyObject

public final class ExpirableLazyObject<T> {

    private final LazyObject<T> value; 
    private final ScheduledThreadPoolExecutor executor;
    private final long expirationDelay;
    private final TimeUnit timeUnit;
    private volatile ScheduledFuture<?> scheduledRemoval;

    public ExpirableLazyObject(Supplier<T> supplier, ScheduledThreadPoolExecutor executor, long expirationDelay, TimeUnit timeUnit) { 
        value = LazyObject.forSupplier(() -> supplier.get());
        this.expirationDelay = expirationDelay;
        this.executor = executor;
        this.timeUnit = timeUnit;
    }

    public T get() { 
        if (scheduledRemoval != null) {
                scheduledRemoval.cancel(true);
        }

        T returnVal = value.get();
        scheduledRemoval = executor.schedule(() -> value.reset(), expirationDelay, timeUnit);

        return returnVal;
    }
}

它需要一个 Supplier 和一些需要的参数来安排值的销毁。每次调用 get() 都会延迟销毁。当然,我可以设计强制客户端通过 GC 处理值的创建和销毁,但我喜欢在内部管理实例的 API。

好处是我可以持久保存缓存对象足够长的时间来支持一个操作,而且我可以懒惰地定期自动刷新参数。

但是我无法摆脱 get() 方法可能存在竞争条件的感觉,但我无法找出确切原因。我不知道我是否需要一些同步块,或者我是否没有正确识别原子性。但是我为消除顾虑而创建的每个同步块都会极大地破坏并发性或引入新的竞争条件。我能看到防止任何竞争条件(如果有的话)的唯一方法是同步整个方法。但这会破坏并发性。这里真的有问题吗?

更新 已经确定存在竞争条件。我想我对如何解决这个问题有一些想法,但我想听听任何能有效完成这个问题并最大限度地提高并发性的建议。

是的,存在竞争条件。

T1:

    //cancel original future
    if (scheduledRemoval != null) {
            scheduledRemoval.cancel(true);
    }

T2:

    //cancel original future AGAIN 
    if (scheduledRemoval != null) {
            scheduledRemoval.cancel(true);
    }

T1:

//set new future (NF)
scheduledRemoval = executor.schedule(() -> value.reset(), expirationDelay, timeUnit);  

T2:

//set even newer future (ENF)
scheduledRemoval = executor.schedule(() -> value.reset(), expirationDelay, timeUnit);

在最后一步中,您用新值覆盖了 scheduledRemoval,但没有取消该未来。任何后续调用都只会看到 ENF,而 NF 将无法访问且无法取消(但仍处于活动状态)。

最简单的解决方案是通过 AtomicReference:

private AtomicReference<ScheduledFuture<?>> scheduledRemoval;

public T get() { 
    ScheduledFuture<?> next = executor.schedule(() -> value.reset(), expirationDelay, timeUnit);
    ScheduledFuture<?> current = scheduledRemoval.getAndSet( next );
    if (current != null) {
            current.cancel(true);
    }

    T returnVal = value.get();

    return returnVal;
}

请注意,您仍然可以 运行 遇到这样的情况:当您想要调用 current.cancel() 时,它已经触发了。避免这种情况需要一些更复杂的信号,我不确定这是否值得。