繁体   English   中英

线程之间的可见性是否需要AtomicReference?

[英]Is AtomicReference needed for visibility between threads?

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

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

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

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

内存一致性影响: 在计数达到零之前,调用countDown()之前的线程中的操作发生在从另一个线程中的相应await()成功返回之后的操作之前

现在有两种选择:

  1. 一旦计数为0,你就永远不会调用onXxx setter方法(即你只调用其中一个方法)并且你不需要任何额外的同步
  2. 或者你可以不止一次调用setter方法,你需要额外的同步

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

如果你在场景1中,你需要决定你想要的防御性:

  • 为了安全起见,你仍然可以使用volatile
  • 如果你很高兴调用代码不会搞乱这个类,你可以使用一个普通变量,但我至少会在方法的javadoc中明确表示只保证第一次调用onXxx方法是可见

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

为了在线程之间显示赋值,必须跨越某种内存屏障。 这可以通过几种不同的方式完成,具体取决于您正在尝试做什么。

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

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

简短的回答是在这里不需要AtomicReference。 你需要挥发性的。

原因是你只是写入和读取引用(Result)而不执行任何复合操作,如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(页326),找到以下书籍:

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

或者@Binita Bharti在下面的StackOverflow回答中使用的相同示例
何时在Java中使用AtomicReference?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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