目录
思维导图
1 锁的劣势
使用一致的加锁协议保障对共享状态的安全访问。
当锁的竞争频率非常高,调度和真正用于工作的开销将会非常客观。
加锁是线程安全的,但是也有缺点:
- 加锁导致其它线程不能做任何事情或者被挂起,性能会由于上下文切换和调度受到损失。
- 性能风险-优先级倒置,一个低优先级线程加锁成功,但是持有锁期间发生了错误:调度情况和页错误,将导致其它高优先级线程被阻塞。
2 硬件对并发的支持
独占锁是排它锁,悲观锁。对于细粒度的操作,有更加有效的乐观锁,通过冲突检测判断是否被其它线程修改。
多处理器也提供了特殊的原子指令,来管理并发的访问的共享数据。如获取并增加、交换指令和比较并交换等。
- 这些原子指令的实现多是依赖于硬件的总线加锁或者是缓存锁,来保证对多处理器的协调。
2.1 比较并交换
比较并交换CAS有三个操作数——内存位置V、旧的预期值A和新的更改值B。当A和真实值一致才会更改,否则什么都不做。
下面的demo-1解释了CAS的语义(非实现)。
/**
* 模拟cas
*/
public class SimulatedCas {
private int value;
public synchronized int getValue() {
return value;
}
public synchronized int compareAndSwap(int expectValue, int newValue) {
int oldValue = getValue();
if (expectValue == oldValue) {
value = newValue;
}
return oldValue;
}
public synchronized boolean compareAndSet(int expectValue, int newValue) {
return expectValue == compareAndSwap(expectValue, newValue);
}
}
通常CAS操作都是在循环中进行,当失败会进行重试,不会将线程进行挂起,减少了锁与相关的活跃度风险。
2.2 非阻塞计数器
下列的demo-2利用CAS模拟了线程安全的计数器:
//基于CAS实现的非阻塞计数器
public class CasCounter {
private SimulatedCas simulatedCas;
public int getValue() {
return simulatedCas.getValue();
}
public int increment() {
int v;
do {
v = getValue();
} while (v != simulatedCas.compareAndSwap(v, v+1));
return v+1;
}
}
当成功直接返回,否则进行重试,直到成功。
通常来说基于CAS的计数器性能要远远由于基于锁的计数器。
java的package java.util.concurrent.atomic
已经提供了各种原子类,供我们使用。
3 原子变量类
原子变量类比锁更轻量,因为它不会引起线程的挂起和重新调度。
3.1 原子变量是更佳的volatile
比如我们可以使用原子化来减少竞争条件,比如下列的demo-3:
public class CasNumberRange {
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 newValue) {
while (true) {
int upper = getUpper();
IntPair old = values.get();
if (upper<newValue) {
throw new IllegalStateException();
}
IntPair intPair = new IntPair(newValue, upper);
if (values.compareAndSet(old, intPair)) {
return;
}
}
}
@Data
private static class IntPair {
final int lower;
final int upper;
}
}
通过原子化的compareAndSet底层调用比较并交换来实现更新边界条件。
4 非阻塞算法
一个线程的失败和挂起不应该影响其它线程的失败和挂起,这样的算法被称为非阻塞。
在线程间使用CAS进行协调,可以构建非阻塞的算法。
4.1 非阻塞栈
创建非阻塞算法的前提是维持数据的一致性。
下列demo-4演示了如何使用原子引用来构建栈:
/**
* 使用cas的非阻塞栈
* @param <E>
*/
public class ConcurrentStack<E> {
private final AtomicReference<Node<E>> top = new AtomicReference<>();
public void push(E item) {
Node<E> oldNode;
Node<E> newNode = new Node<>(item);
do {
oldNode = top.get();
newNode.next = oldNode;
}while (!top.compareAndSet(oldNode, newNode));
}
public E pop() {
Node<E> oldNode;
Node<E> newHead;
do {
oldNode = top.get();
if (oldNode == null) {
return null;
}
newHead = oldNode.next;
} while (!top.compareAndSet(oldNode, newHead));
return oldNode.item;
}
private static class Node<E> {
private final E item;
private Node next;
public Node(E item) {
this.item = item;
}
}
}
这里我们把原子化范围缩到唯一变量栈顶top,出栈和入栈就是对栈顶top的原子化更新。从而实现非阻塞的栈。
这里ConcurrentStack之所以线程安全,主要来源于:compareAndSet提供的原子性和可见性保证。
4.2 非阻塞链表
下面的demo-5演示了一个非阻塞链表的插入过程:
public class LinkedQueue<E> {
private static class Node<E> {
final E item;
final AtomicReference<Node<E>> next;
public Node(E item, AtomicReference<Node<E>> next) {
this.item = item;
this.next = next;
}
}
private final Node<E> dummy = new Node<>(null, null);
private final AtomicReference<Node<E>> head = new AtomicReference<>(dummy);
private final AtomicReference<Node<E>> tail = new AtomicReference<>(dummy);
/**
* 非阻塞队列
* @param item
* @return
*/
public boolean put(E item) {
Node<E> eNode = new Node<>(item, null);
while (true) {
Node<E> curTail = tail.get();
Node<E> tailNext = curTail.next.get();
//如果不相等,说明被其它线程修改,再次循环
if (curTail == tail.get()) {
//如果不为null,说明上一个线程插入新节点,但是cas修改tail失败,需要帮助修改tail
if (tailNext != null) {
tail.compareAndSet(curTail, tailNext);
} else {
//可以插入新节点
if (curTail.next.compareAndSet(null, eNode)) {
//尝试修改tail,失败成功都会返回,由其它线程put时会进行判断
tail.compareAndSet(curTail, eNode);
return true;
}
}
}
}
}
}
这里主要是由几个步骤保证了线程安全:
- 首先获取tail节点,判断是否修改,如果被修改进行重试,否则进入下一步。
- 判断tail的next是否为null,正常为null,不为null说明其它线程正在进行修改或者修改失败了,此时需要协助cas更改tail到next的节点。
- 否则可以进行正常的新节点插入,通过cas更改tail的next,如果成功进行下一步,否则重试。
- 改变tail指针到新节点,这里不管cas成功失败,都会返回true。因为我们在循环开始进行了判断,进行处理tail的指向问题。
4.3 ABA问题
ABA问题是因为算法误用比较并交换而引起的反常现象。
可以在更新引用变量时,加上一个版本号的更新。
参考文献
[1].《JAVA并发编程实战》.