简体   繁体   中英

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

A while back, I created a Java class that encapsulates lazy initialization into a LazyObect . It maintains concurrency and threadsafety and also provides a reset() method to clear the value.

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

I wanted to try and take this concept a little farther. If the LazyObject is holding a very large object that is used only for a short time (like a memory-intensive hashmap), I want the value to be destroyed after a period of no use. If it ends up being used again later, then it will initialize again and reschedule another destruction cycle.

So I created 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;
    }
}

It takes a Supplier and some needed arguments to schedule the destruction of the value. It will delay the destruction every time get() is called. Of course, I could do designs that force the client to handle the creation and destruction of the value via GC, but I like APIs that manage the instances internally.

The benefits are I can persist cached objects long enough to support an operation, and I can lazily and regularly refresh parameters automatically.

However I cannot shake the feeling the get() method might have a race condition, but I cannot figure out an exact reason why. I don't know if I need some synchronized blocks or if I am not properly identifying atomicity. But every synchronized block I made to appease my concerns would greatly undermine concurrency or introduce a new race condition. The only way I can see to prevent any race condition (if there is one) is to synchronize the entire method. But that would undermine concurrency. Is there really a problem here?

UPDATE It has been established there is a race condition. I think I have a few ideas on how to fix this, but I'd like to hear any suggestions that would efficiently accomplish this and maximize concurrency.

Yes, there is a race condition.

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

In the last step you overwrite scheduledRemoval with a new value without cancelling that future. Any subsequent calls will only see ENF , while NF will be inaccessible and uncancellable (but still active).

The easiest solution is via 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;
}

Note that you can still run into a situation where by the time you'd want to call current.cancel() , it already fired. Avoiding that would require some more complicated signalling, which I'm not sure is worth it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM