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

时间:2015-12-01 04:25:20

标签: java multithreading

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

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

4 个答案:

答案 0 :(得分:2)

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

答案 1 :(得分:1)

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

  

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

现在有两种选择:

  1. 一旦计数为0,你就永远不会调用onXxx setter方法(即你只调用其中一个方法)并且你不需要任何额外的同步
  2. 或者您可以多次调用setter方法,并且需要额外的同步
  3. 如果您在方案2中,则需要至少volatile变量(在您的示例中不需要AtomicReference)。

    如果你在场景1中,你需要决定你想要的防守程度:

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

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

答案 2 :(得分:0)

为了使分配在线程之间可见,必须跨越某种类型的内存屏障。这可以通过几种不同的方式完成,具体取决于您尝试做什么。

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

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

答案 3 :(得分:0)

简短的回答你不这里需要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并发(页326),找到以下书籍:

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

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