我希望懒洋洋地创建一些东西并将结果缓存为优化。下面的代码是安全有效的,还是有更好的方法来做到这一点?这里需要比较和设置循环吗?
...
AtomicReference<V> fCachedValue = new AtomicReference<>();
public V getLazy() {
V result = fCachedValue.get();
if (result == null) {
result = costlyIdempotentOperation();
fCachedValue.set(result);
}
return result;
}
编辑:在这个例子中,来自expensiveIdempotentOperation()的值总是相同的,无论是什么线程都调用它。
答案 0 :(得分:10)
这不是一个很棒的系统。问题是两个线程可能会找到result == null
,并且都会将fCachedValue
设置为新的结果值。
您想使用compareAndSet(...)方法:
AtomicReference<V> fCachedValue = new AtomicReference<>();
public V getLazy() {
V result = fCachedValue.get();
if (result == null) {
result = costlyIdempotentOperation();
if (!fCachedValue.compareAndSet(null, result)) {
return fCachedValue.get();
}
}
return result;
}
如果多个线程在初始化之前进入该方法,则它们都可能尝试创建大型结果实例。他们都将创建自己的版本,但完成该过程的第一个将是将结果存储在AtomicReference中的人。其他线程将完成他们的工作,然后处置他们的result
,而是使用“获胜者”创建的result
实例。
答案 1 :(得分:3)
出于类似目的,我实施了OnceEnteredCallable,为结果返回ListenableFuture
。优点是其他线程没有被阻塞,这种代价高昂的操作被调用一次。
用法(需要番石榴):
Callable<V> costlyIdempotentOperation = new Callable<>() {...};
// this would block only the thread to execute the callable
ListenableFuture<V> future = new OnceEnteredCallable<>().runOnce(costlyIdempotentOperation);
// this would block all the threads and set the reference
fCachedValue.set(future.get());
// this would set the reference upon computation, Java 8 syntax
future.addListener(() -> {fCachedValue.set(future.get())}, executorService);
答案 2 :(得分:3)
这会answer向@TwoThe展开AtomicReference<Future<V>>
的使用方式。
基本上,如果您不介意在代码中使用(稍微贵一点)synchronized
个部分,那么最简单(也是最易读)的解决方案就是使用Double-checked Locking成语(与volatile
)。
如果您仍想使用CAS(这是Atomic*
类型的全部内容),您必须使用AtomicReference<Future<V>>
,而不是AtomicReference<V>
(或者你可能最终有多个线程计算相同的昂贵价值)。
但这是另一个问题:您可能获得一个有效的Future<V>
实例并在多个线程之间共享它,但实例本身可能无法使用,因为您的高成本计算可能已失败。这导致我们需要在一些或所有特殊情况下重新设置我们的原子参考(fCachedValue.set(null)
)。
以上暗示仅仅调用fCachedValue.compareAndSet(null, new FutureTask(...))
一次就不够了 - 您必须原子地测试引用是否包含非null
值并重新启动 - 必要时初始化它(在每次调用时)。幸运的是,AtomicReference
类具有getAndUpdate(...)
方法,该方法仅在循环中调用compareAndSet(...)
。因此生成的代码可能如下所示:
class ConcurrentLazy<V> implements Callable<V> {
private final AtomicReference<Future<V>> fCachedValue = new AtomicReference<>();
private final Callable<V> callable;
public ConcurrentLazy(final Callable<V> callable) {
this.callable = callable;
}
/**
* {@inheritDoc}
*
* @throws Error if thrown by the underlying callable task.
* @throws RuntimeException if thrown by the underlying callable task,
* or the task throws a checked exception,
* or the task is interrupted (in this last case, it's the
* client's responsibility to process the cause of the
* exception).
* @see Callable#call()
*/
@Override
public V call() {
final RunnableFuture<V> newTask = new FutureTask<>(this.callable);
final Future<V> oldTask = this.fCachedValue.getAndUpdate(f -> {
/*
* If the atomic reference is un-initialised or reset,
* set it to the new task. Otherwise, return the
* previous (running or completed) task.
*/
return f == null ? newTask : f;
});
if (oldTask == null) {
/*
* Compute the new value on the current thread.
*/
newTask.run();
}
try {
return (oldTask == null ? newTask : oldTask).get();
} catch (final ExecutionException ee) {
/*
* Re-set the reference.
*/
this.fCachedValue.set(null);
final Throwable cause = ee.getCause();
if (cause instanceof Error) {
throw (Error) cause;
}
throw toUnchecked(cause);
} catch (final InterruptedException ie) {
/*
* Re-set the reference.
*/
this.fCachedValue.set(null);
/*
* It's the client's responsibility to check the cause.
*/
throw new RuntimeException(ie);
}
}
private static RuntimeException toUnchecked(final Throwable t) {
return t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t);
}
}
<强> P上。 S。您可能还想查看CompletableFuture
类。
答案 3 :(得分:2)
在使用辅助原子布尔值执行昂贵的操作(tm)之前,您可以正确地仔细检查,如下所示:
AtomicReference<V> fCachedValue = new AtomicReference<>();
AtomicBoolean inProgress = new AtomicBoolean(false);
public V getLazy() {
V result = fCachedValue.get();
if (result == null) {
if (inProgress.compareAndSet(false, true)) {
result = costlyIdempotentOperation();
fCachedValue.set(result);
notifyAllSleepers();
} else {
while ((result = fCachedValue.get()) == null) {
awaitResultOfSet(); // block and sleep until above is done
}
}
}
return result;
}
即使没有设置值,这也不会阻止线程阻塞,它至少可以保证计算只进行一次。阻塞也意味着CPU可用于其他任务。但请注意,如果您使用标准的wait / notify,这可能会导致线程锁定,如果第一个通知,之后另一个等待。您可以执行wait(T_MS)
或使用更为复杂的工具,例如AtomicReference<Future<V>>
。
答案 4 :(得分:1)
正如@rolfl指出的那样,在基于CAS的方法下,多个线程可能会创建自己的result
实例,这应该是昂贵的。
众所周知的解决方案是使用基于锁的延迟初始化模式。它使用单个锁,它可以处理在持有锁时抛出的异常异常,因此如果正确应用,这种方法可以解除与锁定相关的大多数复杂问题。
答案 5 :(得分:1)
您只需要一个synchronized
块和其中的第二个null检查即可。
AtomicReference<V> fCachedValue = new AtomicReference<>();
private final Object forSettingCachedVal = new Object();
public V getLazy() {
V result = fCachedValue.get();
if (result == null) {
// synchronizing inside the null check avoids thread blockage
// where unnecessary, and only before initialization.
synchronized(forSettingCachedVal) {
// because the thread may have waited for another thread
// when attempting to enter the synchronized block:
result = fCachedValue.get();
// check that this was the first thread to enter the
// synchronized block. if not, the op is done, so we're done.
if (result != null) return result;
// the first thread can now generate that V
result = costlyIdempotentOperation();
// compareAndSet isn't strictly necessary, but it allows a
// subsequent assertion that the code executed as expected,
// for documentation purposes.
boolean successfulSet = fCachedValue.compareAndSet(null, result);
// assertions are good for documenting things you're pretty damn sure about
assert successfulSet : "something fishy is afoot";
}
}
return result;
}
此解决方案尽管比rolfl's复杂一些,但可以避免多次执行昂贵的操作。因此:
答案 6 :(得分:1)
尝试AtomicInitializer或AtomicSafeInitializer:
class CachedValue extends AtomicInitializer<V> {
@Override
public V initialize() {
return costlyIdempotentOperation();
}
}