【问题标题】:Is AtomicReference needed for visibility between threads?线程之间的可见性是否需要 AtomicReference?
【发布时间】:2016-03-04 21:57:22
【问题描述】:

我正在使用一个在发送请求时需要回调的框架。每个回调都必须实现这个接口。回调中的方法是异步调用的。

public interface ClientCallback<RESP extends Response>
{
  public void onSuccessResponse(RESP resp);

  public void onFailureResponse(FailureResponse failure);

  public void onError(Throwable e);
}

要使用 TestNG 编写集成测试,我想要一个阻塞回调。所以我使用了一个 CountDownLatch 在线程之间进行同步。

这里真的需要 AtomicReference 还是可以使用原始引用?我知道如果我使用原始引用和原始整数(而不是 CountDownLatch),代码将无法工作,因为可见性不保证。但由于 CountDownLatch 已经同步,我不确定是否需要来自 AtomicReference 的额外同步。 注意:Result 类是不可变的。

public class BlockingCallback<RESP extends Response> implements ClientCallback<RESP>
{
  private final AtomicReference<Result<RESP>> _result = new AtomicReference<Result<RESP>>();
  private final CountDownLatch _latch = new CountDownLatch(1);

  public void onSuccessResponse(RESP resp)
  {
    _result.set(new Result<RESP>(resp, null, null));
    _latch.countDown();
  }

  public void onFailureResponse(FailureResponse failure)
  {
    _result.set(new Result<RESP>(null, failure, null));
    _latch.countDown();
  }

  public void onError(Throwable e)
  {
    _result.set(new Result<RESP>(null, null, e));
    _latch.countDown();
  }

  public Result<RESP> getResult(final long timeout, final TimeUnit unit) throws InterruptedException, TimeoutException
  {
    if (!_latch.await(timeout, unit))
    {
      throw new TimeoutException();
    }
    return _result.get();
  }

【问题讨论】:

  • 不,不需要。任何形式的安全发布都可以使用(但原始参考不是 OK)。 C.f.这个堆栈溢出问题:stackoverflow.com/questions/801993/…
  • @markspace 你会推荐一个 volatile 变量吗?
  • 实际上,这可能很棘手。 countDow() 保证在对await() 的任何调用之前发生。这里可以使用原始引用,因为每次写入_result 之后都会调用countDown,而读取_result 之前会调用await。所以_result 实际上可以使用_latchs 可见性语义。 Brian Goetz 称此为捎带支持。 stackoverflow.com/questions/18732088/…
  • 实际上,如果这是现有的工作代码,我建议不要更改它。你不会有任何收获。如果您假设性地询问,那么 volatile 将起作用。捎带也有效,但很难发现。维护程序员很容易错过它。我建议对代码进行正式审查。如果没有人理解捎带,那么将其保留为 AtomicReference。
  • @Markspace 你在那里很困惑。同步锁存器的要点之一是提供可见性保证。如果您开始不依赖这些,则必须在任何地方使用 AtomicReferences 或 volatile。显然,原始参考是正确的选择,留下其他内容只会让未来的读者感到困惑。

标签: java multithreading


【解决方案1】:

你不需要在这里使用另一个同步对象(AtomicRefetence)。关键是在一个线程中调用 CountDownLatch 之前设置该变量,并在另一个线程中调用 CountDownLatch 之后读取该变量。 CountDownLatch 已经进行了线程同步,并调用了内存屏障,所以保证了先写后读的顺序。因此,您甚至不需要为该字段使用 volatile。

【讨论】:

  • 这取决于 setter 是否被多次调用 - 如果是,则无法保证可见性,您需要额外的同步。
【解决方案2】:

为了使分配在线程中可见某种必须跨越内存屏障。这可以通过几种不同的方式来完成,具体取决于您要执行的操作。

  • 您可以使用volatile 字段。对 volatile 字段的读取和写入是原子性的,并且跨线程可见。
  • 您可以使用AtomicReference。这实际上是the same as a volatile field,但更灵活一些(您可以重新分配和传递对AtomicReference 的引用)并且有一些额外的操作,例如compareAndSet()
  • 您可以使用CountDownLatch 或类似的synchronizer 类,但您需要密切注意它们提供的内存不变量。例如,CountDownLatch 保证await() 的所有线程将看到调用countDown() 的线程中发生的所有内容,直到调用countDown()
  • 您可以使用synchronized 块。这些更加灵活,但需要更加小心 - 写入都必须是synchronized,否则可能看不到写入。
  • 您可以使用线程安全集合,例如ConcurrentHashMap。如果您只需要一个跨线程引用,那就大材小用了,但对于存储多个线程需要访问的结构化数据很有用。

这并不是一个完整的选项列表,但希望您能看到有几种方法可以确保值对其他线程可见,AtomicReference 只是其中一种机制。

【讨论】:

  • 或者你也可以使用捎带。 ;-) 在这种情况下,“原始参考”会做什么。参考文献Java 并发实践,作者 Brian Goetz。
  • CountDownLatch 不是“线程控制类”。它使用与您的所有其他示例相同的 synchronizes-withhappen-before 语义。换句话说,捎带在所有这些情况下都会自然发生,并且可以被其中任何一个使用。
  • @markspace 感谢您的澄清。虽然所有这些都是相同内存语义的示例,但它们在实践中是不同的(即,您可能更喜欢一种或另一种,具体取决于具体的用例)。我在这里的意图只是提出一些替代工具。
  • 别担心,我希望我没有听起来刻薄之类的。我只是想小心术语。人们阅读文字并获得有趣的想法,他们倾向于携带这些有趣的想法而从未纠正它们。重要的是要了解,在 OPs 案例中,捎带是有效的,它之所以有效是因为happens-before 是可传递的。它总是这样工作(除了 final 字段,它们不是传递的)。
【解决方案3】:

简短的回答是您不需要在这里需要 AtomicReference。不过你需要 volatile。

原因是您只是对引用(结果)进行写入和读取,而没有执行任何复合操作,例如 compareAndSet()。

对于引用变量和大多数原始变量(除了 long 和 double 之外的所有类型),读取和写入都是原子的。

参考, Sun Java 教程
https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

然后是 JLS(Java 语言规范)

对引用的写入和读取始终是原子的,无论它们是作为 32 位还是 64 位值实现的。

Java 8
http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.7
Java 7
http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7
Java 6
http://docs.oracle.com/javase/specs/jls/se6/html/memory.html#17.7

来源:https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

原子动作不能交错,因此可以使用它们而不必担心线程干扰。但是,这并不能消除同步原子操作的所有需要​​,因为仍然可能出现内存一致性错误。使用 volatile 变量可降低内存一致性错误的风险,因为对 volatile 变量的任何写入都会与后续读取同一变量的操作建立起先发生关系。这意味着对 volatile 变量的更改始终对其他线程可见。更重要的是,这还意味着当一个线程读取一个 volatile 变量时,它不仅会看到 volatile 的最新更改,还会看到导致更改的代码的副作用。

由于您只有单个操作写入/读取并且它是原子的,因此将变量设置为 volatile 就足够了。

关于CountDownLatch的使用,它用于等待其他线程中的n个操作完成。由于您只有一个操作,因此您可以使用 Condition,而不是 CountDownLatch。

如果您对 AtomicReference 的使用感兴趣,可以查看 Java Concurrency in Practice (Page 326),找到以下书籍:

https://github.com/HackathonHackers/programming-ebooks/tree/master/Java

或@Binita Bharti 在 StackOverflow 回答中使用的相同示例
When to use AtomicReference in Java?

【讨论】:

  • 请提供解释。
  • 我应该说:volatile 不是必要需要的——闩锁的可见性保证可能就足够了。
  • latch如何保证另一个变量的可见性?
  • @assylias 刚刚看到您对 CountdownLatch Javadoc 的引用,是的,在这种情况下,此处不需要 volatile。
【解决方案4】:

一个好的起点是javadoc(强调我的):

内存一致性效果:直到计数达到零,在调用countDown()之前线程中的操作@ happen-before从另一个线程中对应的await() 成功返回。

现在有两种选择:

  1. 要么在计数为 0 时永远不调用 onXxx setter 方法(即只调用其中一个方法一次),并且不需要任何额外的同步
  2. 或者您可能会多次调用 setter 方法并且确实需要额外的同步

如果您处于方案 2 中,则需要使变量至少为 volatile(在您的示例中不需要 AtomicReference)。

如果您处于场景 1 中,您需要决定自己的防御程度:

  • 为了安全起见,您仍然可以使用volatile
  • 如果您很高兴调用代码不会与类混淆,您可以使用普通变量,但我至少会在方法的 javadoc 中明确说明只有第一次调用 onXxx方法保证可见

最后,在场景 1 中,您可能希望强制执行 setter 只能调用一次这一事实,在这种情况下,您可能会使用 AtomicReference 及其 compareAndSet 方法来确保引用为空提前,否则抛出异常。

【讨论】:

    猜你喜欢
    • 2013-04-22
    • 2012-03-11
    • 1970-01-01
    • 2014-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-16
    • 2016-03-01
    相关资源
    最近更新 更多