锁使用起来比较方法,但是它存在一些问题:
volatile显然是一种非常轻量的同步操作,它不会引起上下文切换,但它无法支持复合操作,例如i++,看起来是一条指令,实际上是三条字节码指令。所以它也无法支持“测试并更新”这种在并发世界里极为重要但复合操作。
有没有一种粒度更细,类似于volatile,又支持原子更新操作的机制呢?
原子变量运而生。它是对于常规变量的一种封装,利用底层native接口,提供了变量上的原子一系列复合操作,例如i++,CAS等。具体参考CAS和原子类。原子变量本质上是用硬件实现的“测试和更新”复合原子操作,并且,在大量精巧的非阻塞并发程序中,核心就是将一致性问题一步步缩小范围到某一组原子变量上面。
如果在算法的每个步骤中都存在某个线程能够执行下去,这种算法就被称为无锁(Lock-Free)算法。
不管是非阻塞算法还是无锁算法,都是针对锁的活跃性问题衍生出来的概念。这种算法通常比锁更难设计,核心就是将一致性问题一步步缩小范围到某一组原子变量上面。
那么,如何利用原理变量设计更好的非阻塞机制呢?我们来看一些案例。
@ThreadSafe
public class CasCounter {
private SimulatedCAS value;
public int getValue() {
return value.get();
}
public int increment() {
int v;
do{
v = value.get();
// CAS更新,避免更新丢失
} while (v != value.compareAndSwap(v, v + 1));
return v + 1;
}
}
public class CasNumberRange {
// 1. 保证复合不变性条件,lower和upper必须作为一个整体更新
@Immutable
private static class IntPair {
final int lower; // Invariant: lower <= upper
final int upper;
}
private final AtomicReference<IntPair> values = new AtomicReference<IntPair>(new IntPair(0, 0));
public int getLower() {
return values.get().lower;
}
public int getUpper() {
return values.get().upper;
}
public void setLower(int i) {
// 2. CAS防止更新丢失
while (true) {
IntPair oldv = values.get();
if (i > oldv.upper)
throw new IllegalArgumentException(
"Can't set lower to " + i + " > upper");
IntPair newv = new IntPair(i, oldv.upper);
if (values.compareAndSet(oldv, newv))
return;
// similarly for setUpper
}
}
}
@ThreadSafe
public class ReentrantLockPseudoRandom extends PseudoRandom {
private final Lock lock = new ReentrantLock(false);
private int seed;
ReentrantLockPseudoRandom(int seed) {
this.seed = seed;
}
public int nextInt(int n) {
lock.lock();
try {
int s = seed;
seed = calculateNext(s);
int remainder = s % n;
return remainder > 0 ? remainder : remainder + n;
} finally {
lock.unlock();
}
}
}
@ThreadSafe
public class AtomicPseudoRandom extends PseudoRandom {
private AtomicInteger seed;
AtomicPseudoRandom(int seed) {
this.seed = new AtomicInteger(seed);
}
public int nextInt(int n) {
while (true) {
int s = seed.get();
int nextSeed = calculateNext(s);
if (seed.compareAndSet(s, nextSeed)) {
int remainder = s % n;
return remainder > 0 ? remainder : remainder + n;
}
}
}
}
这边我们可以研究一下CAS方式的可伸缩性,讨论一下CAS和Lock怎么选用。
可以看到,在竞争非常激烈的时候,CAS(Atomic)的方式实际上比锁还慢,这是因为竞争激烈的时候产生了大量的自旋。但是实际情况不会是这样——不会有程序除了竞争锁啥也不干,因此CAS的可伸缩性要更接近第二个图的情况,通常要好得多。
但讨论到这里,还没说完全,CAS和Lock的选用,主要还是要考虑自旋的开销:**自旋的时间和竞争的激烈程度,和获得锁的时间都有关系(例如io设备的锁,可能会导致长时间的自旋)。**所以,如果持有锁进行耗时操作,那么使用lock也没什么不好,lock的也更容易保证正确性,即使会有性能损耗。
栈基于头插法,头插法的非阻塞算法比较好实现,可以对比下边链表的尾插法的实现。
@ThreadSafe
public class ConcurrentStack<E> {
AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();
public void push(E item) {
Node<E> newHead = new Node<E>(item);
Node<E> oldHead;
// 自旋CAS保证push不丢失
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node<E> oldHead;
Node<E> newHead;
// 自旋CAS保证pop不丢失
do {
oldHead = top.get();
if (oldHead == null)
return null;
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
private static class Node<E> {
public final E item;
public Node<E> next;
public Node(E item) {
this.item = item;
}
}
}
链表的操作更加复杂,例如中间插入,那么需要保证两个指针更新操作的原子性;又或者,链表的尾部插入也需要更新两个指针(连接新节点,移动尾部指针)。这些用CAS来设计都相对比较复杂。
@ThreadSafe
public class LinkedQueue<E> {
private static class Node<E> {
final E item;
final AtomicReference<Node<E>> next;
public Node(E item, Node<E> next) {
this.item = item;
this.next = new AtomicReference<Node<E>>(next);
}
}
private final Node<E> dummy = new Node<E>(null, null);
private final AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(dummy);
private final AtomicReference<Node<E>> tail = new AtomicReference<Node<E>>(dummy);
public boolean put(E item) {
Node<E> newNode = new Node<E>(item, null);
// 自旋CAS防止更新丢失
while (true) {
Node<E> curTail = tail.get();
Node<E> tailNext = curTail.next.get();
if (curTail == tail.get()) {
if (tailNext != null) {
// 帮助推进尾部指针
// Queue in intermediate state, advance tail
tail.compareAndSet(curTail, tailNext);
} else {
// 插入新节点
// In quiescent state, try inserting new node
if (curTail.next.compareAndSet(null, newNode)) {
// 推进尾部指针
// Insertion succeeded, try advancing tail
tail.compareAndSet(curTail, newNode);
return true;
}
}
}
}
}
}
上述是一个尾插法的一个设计实例,中间插入等都是类似的。上述实现看起来比较难懂,实际上它本质上的操作如下:
while(true) {
oldNode = tail;
// 如果有其他线程在这里插入新的尾部节点,则这里会更新失败,重试
if (tail.next.CAS(null, newNode))
tail.CAS(oldNode, newNode)
return
}
上述代码的区别主要是进行了一个小优化:如果当前线程发现取到的tail.next非空,说明有人插入了新的尾节点,这时候它会帮助对方推进尾部指针,加快自己插入的效率,不需要等待对方推进完成才能插入。