jlearning.cn

Java并发性能提升的关键——原子变量与非阻塞同步机制

从这本书的最开始,就接触到原子变量类——AtomicIntegerAtomicReference用于代替锁来实现对安全性的保障。这就是从Java5.0开始的使用原子变量来提供相比较synchronized机制更好的性能和可伸缩性。除此之外,非阻塞的同步机制也能提供这样的良好体验。

锁的劣势

  1. 当线程恢复执行时,必须等待其他线程执行完它们的时间片。在挂起和回复线程过程中存在着很大的开销,通常存在较长时间的中断。如果在类中操作量很小,调度开销和工作开销的比值会非常高。
  2. 一个线程在等待锁的时候,不能做任何事情。如果持有锁的进程被延迟执行(缺页错误,调度延迟等),其他需要这个锁的线程都无法执行下去。
  3. 如果被阻塞的线程优先级较高,持有锁的线程优先级较低,会产生优先级反转。
  4. 如果持有锁的线程被永久的阻塞(死循环,死锁,活锁等),程序永远无法执行下去。

所以需要一种支持volatile变量的可见性,又支持原子性的机制。

硬件对并发的支持

现代的处理器提供了既有volatile的可见性,又有原子性的支持。

早期的处理器支持原子的测试并设置(Test-and-Set)、获取并递增(Fetch-and-Increment)和交换(Swap)指令。现在几乎所有的现代处理器中都包含了原子读-改-写的指令。例如比较并交换(Compare-and-Swap),关联加载/条件存储(Load-Linked/Store-Conditional)。

比较并交换(CAS)

CAS中包含三个操作数——需要读写的内存位置V、进行比较的值A、拟写入的新值B。V的值等于A时,用B来更新V的值,然后返回V的原有值。

当多个线程同时是哟个CAS更新一个变量时,只有一个线程能更新,其他线程都会失败。失败的线程不会被挂起,而是被告知失败,并可以再次尝试。

非阻塞的计数器

使用CAS实现,读取旧值,加1,使用CAS来设置这个新值。如果CAS失败,立即重试。不会发生阻塞,但是如果多个线程同时更新,会多次执行重试。

在竞争程度不高时,基于CAS的计数器性能上远远超过基于锁的计数器。CAS的主要缺点是:它将使调用者处理竞争问题(重试、回退、放弃),而在锁中能自动处理竞争问题。(阻塞)

原子变量类

  1. 更新原子变量的快速路径(非竞争)不会比获取锁的快速路径慢,而它的慢速路径肯定比锁的慢速路径快,因为不需要挂起或重新调度线程。
  2. 在发生竞争的情况下,能提供更高的可伸缩性,因为直接利用了硬件对并发的支持。

非阻塞算法

在基于锁的算法中可能会发生各种活跃性故障,如果线程在持有锁时由于阻塞IO内存缺页等导致推迟执行,那么很可能所有线程都不能继续执行下去。如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。

非阻塞的栈

push方法创建一个新的节点,该节点的next域只想当前的栈顶,然后使用CAS把这个新节点放入栈顶。如果在开始插入节点之前,位于栈顶的节点没有发生变化,那么CAS成功,如果栈顶节点发生了变化,失败,而push方法会根据栈的当前状态来更新节点,并且再次尝试。

非阻塞的链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.compareAndSer(null,newNode)){
//插入操作成功,尝试推进尾结点
tail.compareAndSer(curTail,newNode);
return true;
}
}
}
}
}