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实现典型的并发数据结构。