AtomicReference,一个有时比同步块更简单的选择

Brian Goetz 在高级主题部分的他的Java Concurrency in Practice一书中列出了AtomicReference。但是,我们将看到对于特定的用例,AtomicReference比同步块更易于使用。新的JDK 8 getAndUpdate和updateAndGet方法使AtomicReference更加易于使用。

但是,让我们从第一个主题开始,这个用例可以通过AtomicReference实现,而不是通过同步块来实现:并发状态机。

如何使用compareAndSet:并发状态机

来自maven surefire插件的CommandReader类使用compareAndSet方法实现并发状态机:


public final class CommandReader {
  private static final CommandReader READER = new CommandReader();
  private final Thread commandThread = 
    newDaemonThread( new CommandRunnable(), "surefire-forkedjvm-command-thread" );
  private final AtomicReference<Thread.State> state =
   new AtomicReference<Thread.State>( NEW );
  public static CommandReader getReader() {
     final CommandReader reader = READER;
     if ( reader.state.compareAndSet( NEW, RUNNABLE ) ) {
         reader.commandThread.start();
     }
   return reader;
  }
}

AtomicReference类包装了另一个类,以使用原子更新功能来丰富变量。 在第5行中,AtomicReference表示枚举类型Thread.State的原子变量。 在第6行中将AtomicReference初始化为值NEW。

getReader方法必须在当前状态为新状态时启动命令线程,并将其值更新为RUNNABLE。由于该方法可以由多个线程并行调用,因此设置和检查必须以原子方式完成。这是由compareAndSet方法完成的,第9行。compareAndSet方法仅在当前值与预期值相同时才将其值更新为新值。在本例中,它只在当前值为新值时将变量更新为RUNNING。如果更新成功,该方法将返回true,而线程将启动,否则,它将返回false,并且什么也不会发生。检查和更新是自动完成的。

作为比较,这里是使用同步块实现的相同功能。


public final class CommandReader {
 private static final CommandReader READER = new CommandReader();
 private final Thread commandThread = 
   newDaemonThread( new CommandRunnable(), "surefire-forkedjvm-command-thread" );
 private final  Thread.State state =  NEW;
 private final Object LOCK = new Object();
 public static CommandReader getReader()  {
    final CommandReader reader = READER;
    synchronized(reader.LOCK) {
      if(reader.state == NEW) {
         reader.commandThread.start();
         reader.state = RUNNABLE;
      }
    }
    return reader;
 }
}

我们在第10行的变量状态的检查和更新周围使用了一个同步块。该示例说明了为什么我们需要原子检查和更新。 如果没有同步,则两个线程可能会多次读取来自CommandThread的start方法的NEW状态,从而调用start方法。

如我们所见,我们可以通过对compareAndSet的一种方法调用来替换同步块、if语句和对状态的写入。 在下一个示例中,我们看到如何使用compareAndSet方法更新值。

更新值:重试直到成功

使用compareAndSet进行更新的想法是重试直到更新成功。 RXJava的AsyncProcessor类使用此技术来更新方法add中的订阅者数组:


final AtomicReference<AsyncSubscription<T>[]> subscribers;
boolean add(AsyncSubscription<T> ps) {
 for (;;) {
   AsyncSubscription<T>[] a = subscribers.get();
   if (a == TERMINATED) {
     return false;
   }
   int n = a.length;
   @SuppressWarnings("unchecked")
   AsyncSubscription<T>[] b = new AsyncSubscription[n + 1];
   System.arraycopy(a, 0, b, 0, n);
   b[n] = ps;
   if (subscribers.compareAndSet(a, b)) {
     return true;
    }
  }
}

使用for循环(第3行)重试更新。仅当订户数组处于第6行的终止状态时,或第14行的compareAndSet操作成功时,才终止循环。在所有其他情况下,更新都在数组的副本上重复。

从JDK 8开始,AtomicReference类在两个实用程序方法getAndUpdate和updateAndGet中提供此功能。下面显示了JDK 8中getAndUpdate方法的实现:


public final V getAndUpdate(UnaryOperator<V> updateFunction) {
 V prev, next;
 do {
   prev = get();
   next = updateFunction.apply(prev);
 } while (!compareAndSet(prev, next));
 return prev;
}

该方法使用与AsyncProcessor类中的add方法相同的技术。 它在第6行的循环中重试compareAndSet方法。更新失败时,将多次调用updateFunction。 因此,此功能必须没有副作用或幂等。

这是上面用新的updateAndGet方法实现的add方法:


boolean add(AsyncSubscription<T> ps) {
AsyncSubscription<T>[] result = subscribers.updateAndGet(  ( a ) ->  {  
  if (a != TERMINATED) {       
    int n = a.length;
    @SuppressWarnings("unchecked")
    AsyncSubscription<T>[] b = new AsyncSubscription[n + 1];
    System.arraycopy(a, 0, b, 0, n);
    b[n] = ps;
    return b;
  }
  else {
    return a;
  }
});
return result != TERMINATED;    
}

如我们所见,while循环隐藏在updateAndGet方法中。 我们只需要实现一个从旧值计算新值的函数。

总结

我们已经看到了compareAndSet的两个示例。如果您对更多示例感兴趣,请阅读《多处理器编程的艺术》一书。它显示了如何使用compareAndSet实现典型的并发数据结构。

原文链接: https://dev.to//vmlensd/atomicreference-a-sometimes-easier-alternative-to-synchronized-blocks-538c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值