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.