【有书共读】《JAVA并发编程实战》第15章
第十五章 原子变量与阻塞同步机制
- 锁 & volatile & CAS 比较
- 锁机制的不足
- 如果取到锁之后的***作短小,则线程调度的开销占比太大
- 等待锁时,线程被挂起,不能做事
- 会出现有线程优先级反转的情况
- volatile
- 不需要线程上下文切换,但是很多细小的***作都不具有原子性,无法直接地有效完成,如: i++
- CAS***作:
- 底层CPU支持比较并交换(Compare And Swap)指令,可以原子性地进行执行取值-判断-写入,保证同时执行的线程只有一个线程能够成功,其余的失败。
- JVM将CAS***作直接编译成机器指令,提高运行速度
- 一次加锁机制比一次CAS执行的指令更多,且可能发生线程调度,开销更大,所以在竞争不激烈的情况下,CAS比加锁开销更小
- CAS将对资源的竞争处理交由调用者来处理,所以使用起来更复杂。
- 锁机制的不足
- 原子变量类(提供了CAS***作的调用)
- 标量类
- 其中的标量类包括:AtomicInteger,AtomicLong,AtomicBoolean,AtomicReference.
- AtomicInteger不是不可变类,支持更新***作,且其hashCode和equals方法都没有重写,因此不适于作为键值。
- AtomicReference< A > a 可以用于同时更新数个需要保持一致状态的值的情况。将这些值封装在A中,a.compareAndSet(oldA, newA)即可更新a的值。其中compareAndSet()方法提供和volatile一样的可见性,get()方法提供votile一样的主内存读取的功能。
- 原子变量类 vs ReentrantLock
- ***作的粒度十分小
- 线程本地的计算量,即***作的粒度,如果很小的话,则线程对资源的竞争将会十分激烈,因为CAS***作失败了的线程马上又会加入下一次对资源的竞争。
- 此时ReentrantLock将竞争资源失败的线程挂起,起到了降低竞争的作用。吞吐率会更高。
- ***作粒度适中
- 随着***作力度变大,CAS***作失败后,需要重新***作再进行下一次资源竞争,竞争激烈性降低。
- 此时原子变量类***作吞吐率更高
- 中低程度资源竞争下,原子变量类的可伸缩性更高(随着线程数量增加,吞吐率降低比ReentrantLock少)。高度竞争情况下,锁机制能更有效地减小竞争。
- ps. CAS在单CPU环境下总是会执行成功。
- ***作的粒度十分小
- 标量类
- 用原子变量类实现四种非阻塞算法
- 非阻塞的栈----只需要将节点的push,pop的CAS判断范围缩小到头结点head是否能成功修改。
- oldNode <-- newNode <-- newHead
- 非阻塞链表队列
- 难点:head-->node1-->node2-->newNode<--newtail , node2<--oldtail 需要将oldtail.next和tail都指向newNode,需要保证两个原子***作都完成,保持一致性。
- oldtail.compareAndSet(null,newNode) 和 tail.compareAndSet(oldtail, newNode),令前一个***作为中间状态。队列要么处于中间状态,要么处于稳定状态,中间状态的线程执行一次tail.compareAndSet(oldtail, oldtail.next)就可以使得队列进入稳定状态。每次先判断队列是否位于中间状态,如果在中间状态的话,就直接将***作 tail.compareAndSet(oldtail, oldtail.next),即把完成一半的另一个线程先完成了。然后就可以继续执行。
- 好处:即使没执行完的线程出现意外而失败了,也不会使得队列在下一个线程执行put时重新进入稳定状态,不一致得到消除。
- 原子的域更新器
- AtomicReferenceFieldUpdater
- ABA问题
- AtomicStampedReference
- 非阻塞的栈----只需要将节点的push,pop的CAS判断范围缩小到头结点head是否能成功修改。
- 总结:非阻塞算法在JAVA中依赖于原子变量类,其可以提供硬件底层支持的原子CAS***作。在资源竞争强度不是特别高的情况下非阻塞算法比锁机制的算法可伸缩性更好。但是非阻塞算法的设计和实现都非常困难(JVM从一个版本升级到另一个版本的过程中,对并发效能的提升主要源自使用非阻塞算法。)