无锁的思想
众所周知, Java 中对并发控制的最常见方法就是锁, 锁能保证同一时刻只能有一个线程访问临界区的资源, 从而实现线程安全. 然而, 锁虽然有效, 但采用的是一种悲观的策略. 它假设每一次对临界区资源的访问都会发生冲突, 当有一个线程访问资源, 其他线程就必须等待, 所以锁是会阻塞线程执行的.
当然, 凡事都有两面, 有悲观就会有乐观. 而无锁就是一种乐观的策略, 它假设线程对资源的访问是没有冲突的, 同时所有的线程执行都不需要等待, 可以持续执行. 如果遇到冲突的话, 就使用一种叫做 CAS (比较交换) 的技术来鉴别线程冲突, 如果检测到冲突发生, 就重试当前操作到没有冲突为止.
CAS 概述
CAS 的全称是 Compare-and-Swap, 也就是比较并交换, 是并发编程中一种常用的算法. 它包含了三个参数: V,A,B.
其中, V 表示要读写的内存位置, A 表示旧的预期值, B 表示新值
CAS 指令执行时, 当且仅当 V 的值等于预期值 A 时, 才会将 V 的值设为 B, 如果 V 和 A 不同, 说明可能是其他线程做了更新, 那么当前线程就什么都不做, 最后, CAS 返回的是 V 的真实值.
而在多线程的情况下, 当多个线程同时使用 CAS 操作一个变量时, 只有一个会成功并更新值, 其余线程均会失败, 但失败的线程不会被挂起, 而是不断的再次循环重试. 正是基于这样的原理, CAS 即时没有使用锁, 也能发现其他线程对当前线程的干扰, 从而进行及时的处理.
CAS 的应用类
Java 中提供了一系列应用 CAS 操作的类, 这些类位于 java.util.concurrent.atomic 包下, 其中最常用的就是 AtomicInteger, 该类可以看做是实现了 CAS 操作的 Integer, 所以, 下面我们就通过学习该类的案例来一窥全貌 CAS 的妙用.
学习 AtomicInteger 之前, 我们先来看一段代码实例:
- public class AtomicDemo {
- public static int NUMBER = 0;
- public static void increase() {
- NUMBER++;
- }
- public static void main(String[] args) throws InterruptedException {
- AtomicDemo test = new AtomicDemo();
- for (int i = 0; i <10; i++) {
- new Thread(() -> {
- for (int j = 0; j <1000; j++)
- test.increase();
- }).start();
- }
- Thread.sleep(200);
- System.out.println(test.NUMBER);
- }
- }
在 main 函数中开启了 10 个线程, 执行后会轮流调用 increase(), 当然我们知道, 运行后输出的结果肯定不是我们期望的值, 因为没有做线程安全的处理, 所以 10 个线程流量操作临界区的资源 NUMBER 就会出错.
解决办法并不难, 用我们之前学过的锁, 例如 synchronized 修饰代码块, 程序就会正常输出 10000. 当然, 用锁解决并不是我们想要的方式, 因为锁会阻塞线程, 影响程序的性能, 这时候, AtomicInteger 就可以派上用场了.
将上面的程序改造一下, 变成下面这样:
- public static AtomicInteger NUMBER = new AtomicInteger(0);
- public static void increase() {
- NUMBER.getAndIncrement();
- }
- public static void main(String[] args) throws InterruptedException {
- AtomicDemo test = new AtomicDemo();
- for (int i = 0; i < 10; i++) {
- new Thread(() -> {
- for (int j = 0; j < 1000; j++)
- test.increase();
- }).start();
- }
- Thread.sleep(200);
- System.out.println(test.NUMBER);
- }
运行 main 方法, 程序输出的就是我们想要的值, 也就是 10000.
上面的代码中, increase 方法里调用了 NUMBER.getAndIncrement() , 这是 AtomicInteger 的自增方法, 会对当前的值加 1, 并且返回旧值, 点进方法的源码, 它调用的是 unsafe.getAndAddInt() 方法:
- public final int incrementAndGet() {
- return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
- }
getAndAddInt 的作用是对当前值加 1, 并返回旧值.
unsafe 是 Unsafe 类的一个变量, 通过 Unsafe.getUnsafe() 来获取
private static final Unsafe unsafe = Unsafe.getUnsafe();
Unsafe 类是一个比较特殊的类, 它是一个 JDK 内部使用的专属类, 用一般的编辑器无法直接查看源码, 只能看到反编译后的 class 文件.
这里要扩展一个知识点, 就是 Java 本身无法访问操作系统, 需要使用 native 方法, 而 Unsafe 类中的方法就包含了大量的 native 方法, 提高了 Java 对系统底层的原子操作能力. 例如我们代码中使用到的 getAndAddInt() 底层就是调用一个 native 方法, 用 idea 点击方法, 得到下面反编译后的代码:
- public final int getAndAddInt(Object var1, long var2, int var4) {
- int var5;
- do {
- var5 = this.getIntVolatile(var1, var2);
- } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
- return var5;
- }
- public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
compareAndSwapInt 的作用是比较并交换整数值, 如果指定的字段的值等于期望值, 也就是 CAS 中的'A' (预期值), 那么就会把它设置为新值 (CAS 中的'B'), 不难想象, 该方法内部的实现必然是依靠原子操作完成的. 除此之外, Unsafe 类中还提供了其他的原子操作的方法, 例如上面源码中的 getIntVolatile 就是使用 volatile 语义获得给定对象的值, 这些方法通过底层的原子操作高效的提升了应用层面的性能.
CAS 的缺点
虽然 CAS 的性能比起锁要强大很多, 但它也存在一些缺点, 例如:
1, 循环的时间开销大
在 getAndAddInt 的方法中, 我们可以看到, 只是简单的设置一个值却调用了循环, 如果 CAS 失败, 会一直进行尝试. 如果 CAS 长时间不成功, 那么循环就会不停的跑, 无疑会给系统造成很大的开销.
2,ABA 问题
前面说过, CAS 判断变量操作成功的条件是 V 的值和 A 是一致的, 这个逻辑有个小小的缺陷, 就是如果 V 的值一开始为 A, 在准备修改为新值前的期间曾经被改成了 B, 后来又被改回为 A, 经过两次的线程修改对象的值还是旧值, 那么 CAS 操作就会误任务该变量从来没被修改过. 这就是 CAS 中的 "ABA" 问题.
当然,"ABA" 问题也有解决方案, Java 并发包中提供了一个带有时间戳的对象引用 AtomicStampedReference, 其内部不仅维护了一个对象值, 还维护了一个时间戳, 当 AtomicStampedReference 对应的数值被修改时, 除了更新数据本身, 还需要更新时间戳, 只有对象值和时间戳都满足期望值, 才能修改成功. 这是 AtomicStampedReference 的几个有关时间戳信息的方法:
- // 比较设置 参数依次为: 期望值 写入新值 期望时间戳 新时间戳
- public boolean compareAndSet(V expectedReference, V newReference,
- int expectedStamp, int newStamp)
- // 获得当前时间戳
- public int getStamp()
- // 设置当前对象引用和时间戳
- public void set(V newReference, int newStamp)
来源: https://www.cnblogs.com/yeya/p/10237376.html