思维导图:
引言:
在此之前,所有对并发竞争的处理都是使用锁来处理的。使用锁固然可以保证修改的正确性,但是,所也有它固有的缺点。锁的缺点如下:
- 高开销:使用锁必然意味着在多个线程竞争时大部分线程都会陷入沉睡然后唤醒的循环,会有极大的开销。
- 长等待:等待中的线程是什么也不能做的,只能等待,这是一种资源的浪费。
- 优先级反转:当低优先级的线程获取到锁后,如果此时高优先级的线程也需要锁,那也必须等待低优先级线程释放才行。
我们可以不使用锁来构建并发程序吗?当然是可以的。这也就是本章的主要内容,使用非阻塞同步机制来构建并发程序。但是,构建非阻塞同步机制来构建并发程序是一件非常复杂的事情,所以,最好还是使用已有的封装类。
使用非阻塞同步机制的核心在于使用原子变量。接下来,本章会分为以下两个部分进行介绍:
- 原理部分:介绍原子变量的简单使用。
- 使用部分:会介绍一些简单的使用非阻塞同步机制构建的数据结构和算法。
一.原子变量类
非阻塞算法在在可伸缩性和活跃性上对比于锁的独占式访问有巨大的性能优势。这些优势来源于多个线程在竞争相同的数据时不会发生阻塞,他能够在更细粒度的层次上进行协调,并且极大的减少调度开销。这在java中是通过原子变量类实现的。接下来,在这个小节中会简要的介绍一下原子变量类。
1.1 比较并交换
首先,第一个问题,非阻塞算法是如何实现在没有阻塞的情况下解决多个线程对同一个数据的竞争的呢?答案就是比较并交换操作。其原理是线程A尝试修改值得时候会检查 此值是否正在被别的线程修改。如果当前值正在被别的线程修改,那么此次更新操作失败,然后,此线程会继续尝试更新,直到成功为止。
比较并交换操作也称为CAS。在处理器中有专门的用于比较并交换操作的指令。其执行逻辑如下:当前需要读写的内存位置为V,进行比较的值A,需要新写入的值B。只有当V的值等于A的值时,才会将内存V的位置的值更新为B,否则不执行任何操作。
我们用java代码来简单模拟一下一上逻辑处理:
@ThreadSafe
public class SimulatedCAS {
@GuardedBy("this") private int value;
public synchronized int get() {
return value;
}
public synchronized int compareAndSwap(int expectedValue, int newValue) {
int oldValue = value;
if (oldValue == expectedValue) {
value = newValue;
}
return oldValue;
}
public synchronized boolean compareAndSet(int expectedValue, int newValue) {
return (expectedValue == compareAndSwap(expectedValue, newValue));
}
}
当然,在JVM中我们可以使用java.util.concurrent.atomic包中的AtomicXXX等原子变量类来进行上述操作,原子变量类已经将CAS操作封装好了。
1.2 更好的volatile
在以前,我们使用不可变对象和volatile来维持包含多个变量的不变性条件,现在,我们可以使用原子变量类来维护不变性条件了。代码如下:
@ThreadSafe
public class CasNumberRange {
@Immutable private static class IntPair {
// 不变性条件: lower <= upper
final int lower;
final int upper;
public IntPair(int lower, int upper) {
this.lower = lower;
this.upper = 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) {
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;
}
}
}
public void setUpper(int i) {
while (true) {
IntPair oldv = values.get();
if (i < oldv.lower) {
throw new IllegalArgumentException("Can't set upper to " + i + " < lower");
}
IntPair newv = new IntPair(oldv.lower, i);
if (values.compareAndSet(oldv, newv)) {
return;
}
}
}
}
1.3 与锁的性能比较
通过以下记这个念头可以看到,在高度竞争的情况下,锁的性能会超过原子变量的性能。但是在更真实的竞争情况下,不会有那么高烈度的竞争,原子变量的性能会超过的锁的性能。
二.非阻塞算法
在这个小节中,我们将会看到一些例子,是关于如何使用原子变量构建非阻塞算法的。这个算法不仅是只有算法,还包括一些数据结构。一般来说,构建非阻塞算法是一件非常复杂的事,不推荐自己构建非阻塞的数据结构。
2.1 非阻塞计数器
以下代码是一个非阻塞计数器,其中SimulatedCAS类已在上文中实现
@ThreadSafe
public class CasCounter {
private SimulatedCAS value;
public int getValue() {
return value.get();
}
public int increment() {
int v;
do {
v = value.get();
} while (v != value.compareAndSwap(v, v + 1));
return v + 1;
}
}
2.2 非阻塞栈
@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;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node<E> oldHead;
Node<E> newHead;
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;
}
}
}
2.3 非阻塞链表
构建非阻塞链表的时候我们会遇见一个问题,即我们需要在一个步骤中包含多个更新操作,类似于事务,以保证数据的一致性。
我们一般用两个技巧来解决这一问题:
- 计时在一个包含多个步骤的更新操作中,也要确保数据结构总是处于一致的状态。
- 如果当B到达时发现A正在修改数据结构,那么在数据结构中因该有足够的信息,使得B能够完成A的更新操作。
以链表的put操作举例,当我们需要put一个新的数据时,需要进行两步的操作,一是将以前的尾节点的next引用指向当前节点,二是将尾节点指向当前节点。
更新时可能会发生这样的情况,线程A操作一成功了,操作二失败了。此时数据的一致性失效了,数据结构是不稳定的。此时若线程B也进行了更新操作。就需要判断数据结构是否处于稳定的状态,如果不稳定,就要让他变得稳定。对于链表来说,是否稳定的判断标准就是尾节点是否执行真正的末尾节点。如下图所示:
如果处于不稳定的状态,那么线程B可以先将线程A的操作执行完毕,然后再来执行自己的操作。代码如下所示:
@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);
while (true) {
Node<E> curTail = tail.get();
Node<E> tailNext = curTail.next.get();
if (curTail == tail.get()) {
if (tailNext != null) {
// 队列处于中间状态,推进尾节点
tail.compareAndSet(curTail, tailNext);
} else {
// 处于稳定状态,尝试插入新节点
if (curTail.next.compareAndSet(null, newNode)) {
// 插入成功,尝试推进尾节点
tail.compareAndSet(curTail, newNode);
return true;
}
}
}
}
}
}