如何以安全有效的方式使用AtomicReference进行延迟创建和设置?

时间:2013-11-20 04:11:43

标签: java concurrency java.util.concurrent

我希望懒洋洋地创建一些东西并将结果缓存为优化。下面的代码是安全有效的,还是有更好的方法来做到这一点?这里需要比较和设置循环吗?

...
AtomicReference<V> fCachedValue = new AtomicReference<>();

public V getLazy() {
    V result = fCachedValue.get();
    if (result == null) {
        result = costlyIdempotentOperation();
        fCachedValue.set(result);
    }
    return result; 
} 

编辑:在这个例子中,来自expensiveIdempotentOperation()的值总是相同的,无论是什么线程都调用它。

7 个答案:

答案 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复杂一些,但可以避免多次执行昂贵的操作。因此:

  1. 昂贵的操作不必是幂等的
  2. 延迟初始化期间的
  3. 线程争用是不可能的,并且
  4. 尽管引入了同步,但是您的代码实际上可能执行得更快。

答案 6 :(得分:1)

尝试AtomicInitializerAtomicSafeInitializer

class CachedValue extends AtomicInitializer<V> {
  @Override
  public V initialize() {
    return costlyIdempotentOperation();
  }
}