对于并发控制而言, 我们平时用的锁 (synchronized,Lock) 是一种悲观的策略. 它总是假设每一次临界区操作会产生冲突, 因此, 必须对每次操作都小心翼翼. 如果多个线程同时访问临界区资源, 就宁可牺牲性能让线程进行等待, 所以锁会阻塞线程执行.
与之相对的有一种乐观的策略, 它会假设对资源的访问是没有冲突的. 既然没有冲突也就无需等待了, 所有的线程都在不停顿的状态下持续执行. 那如果遇到问题了无锁的策略使用一种叫做比较交换 (CAS Compare And Swap) 来鉴别线程冲突, 一旦检测到冲突产生, 就重试当前操作直到没有冲突. CAS 算法是非阻塞的, 它对死锁问题天生免疫, 而且它比基于锁的方式拥有更优越的性能.
CAS 算法的过程是这样: 它包含三个参数 CAS(V,E,N).V 表示要更新的变量, E 表示预期的值, N 表示新值. 仅当 V 值等于 E 值时, 才会将 V 的值设置成 N, 否则什么都不做. 最后 CAS 返回当前 V 的值. CAS 算法需要你额外给出一个期望值, 也就是你认为现在变量应该是什么样子, 如果变量不是你想象的那样, 那说明已经被别人修改过. 你就重新读取, 再次尝试修改即可.
JDK 并发包有一个 atomic 包, 里面实现了一些直接使用 CAS 操作的线程安全的类型. 其中最常用的一个类应该就是 AtomicInteger. 我们以此为例来研究一下没有锁的情况下如何做到线程安全.
private volatile int value;
这是 AtomicInteger 类的核心字段, 代表当前实际取值, 借助 volatile 保证线程间数据的可见性.
获取内部数据的方法:
- public final int get() {
- return value;
- }
我们从源码的实现看看 incrementAndGet()的内部实现
- public final int incrementAndGet() {
- for (;;) {
- int current = get();
- int next = current + 1;
- if (compareAndSet(current, next))
- return next;
- }
- }
代码第二行使用了一个死循环, 原因是:
CAS 的操作未必都是成功的, 因此对于不成功的情况, 我们就需要进行不断的尝试.
第三行取得当前值, 接着 + 1 得到新值 next.
这里我们使用 CAS 必需的两个参数: 期望值以及新值.
使用 compareAndSet()将新值 next 写入. 成功的条件是在写入的时刻当前的值应该要等于刚刚取到的 current. 如果不是这样则说明 AtomicInteger 的值在第 3 行到第 5 行之间被其他线程修改过了. 当前看到的状态是一个过期的状态, 因此返回失败, 需要进行下一次重试, 直到成功为止.
- public final boolean compareAndSet(int expect, int update) {
- return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
- }
整体的过程就是这样子, 利用 CPU 的 CAS 指令, 同时借助 JNI 来完成 Java 的非阻塞算法. 其它原子操作都是利用类似的特性完成的. 大概的逻辑应该是这样:
- if (this == expect) {
- this = update return true;
- } else {
- return false;
- }
CAS 虽然能高效的解决原子问题, 但是 CAS 也会带来 1 个经典问题即 ABA 问题:
因为 CAS 需要在操作值的时候检查下值有没有发生变化, 如果没有发生变化则更新, 但是如果一个值原来是 A, 变成了 B, 又变成了 A, 那么使用 CAS 进行检查时会发现它的值没有发生变化, 但是实际上却变化了.
ABA 问题的解决思路就是使用版本号. 在变量前面追加上版本号, 每次变量更新的时候把版本号加一, 那么 A-B-A 就会变成 1A-2B-3A.
从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题. 这个类在内部不仅维护了对象值, 还维护了一个时间戳(可以是任意的一个整数来表示状态值). 当设置对象值时, 对象值和状态值都必须满足期望值才会写入成功. 因此即使对象被反复读写, 写会原值, 只要状态值发生变化, 就能防止不恰当的写入.
- /**
- * @param expectedReference 期望值
- * @param newReference 写入新值
- * @param expectedStamp 期望状态值
- * @param newStamp 新状态值
- * @return true if successful
- */
- public boolean compareAndSet(V expectedReference,
- V newReference,
- int expectedStamp,
- int newStamp) {
- Pair<V> current = pair;
- return expectedReference == current.reference &&
- expectedStamp == current.stamp &&
- ((newReference == current.reference &&
- newStamp == current.stamp) ||
- casPair(current, Pair.of(newReference, newStamp)));
- }
个人公众号: JAVA 日知录 , http://www.javadaily.cn/
来源: https://www.cnblogs.com/jianzh5/p/11964079.html