编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1String深入理解原理
  • 1.2浮点型数据深入研究
  • 1.3数据装箱和拆箱原理
  • 1.4泛型由来和设计思想
  • 1.5加密和解密设计和原理
  • 2.1面向对象设计思想
  • 2.2抽象类和接口设计
  • 2.3封装和继承设计思想
  • 2.4复用和组合设计思想
  • 2.5对象和引用设计思想
  • 3.1IO流设计思想和原理
  • 3.2为何设计序列化数据
  • 3.3各种拷贝数据比较
  • 3.4高效文件读写的原理
  • 4.1反射性能探索和优化
  • 4.2为何要设计注解思想
  • 4.3动态代理的设计思想
  • 4.4SPI机制设计的思想
  • 4.5异常设计和捕获原理
  • 4.6虚拟机如何处理异常
  • 4.7四种引用设计思想
  • 5.1线程的前世今生探索
  • 5.2线程通信的设计思想
  • 5.3线程监控和Debug设计
  • 5.4线程和JVM之间联系
  • 5.5线程池使用技巧介绍
  • 5.6线程池设计核心原理
  • 5.7线程如何最大优化
  • 6.1多线程并发经典案例
  • 6.2并发安全前世今生
  • 6.3线程安全如何保证
  • 6.4变量的线程安全探索
  • 6.5并发上下文切换原理
  • 6.6理解CAS设计和由来
  • 6.7协程设计思想和原理
  • 6.8事物并发模型解读
  • 6.9并发设计模型研究
  • 6.10并发编程数据一致性
  • 6.11锁问题的定位和修复
  • 6.12多线程如何性能调优
  • 7.1类的加载过程和原理
  • 7.2对象布局设计的原理
  • 7.3双亲委派机制设计思想
  • 7.5代码攻击和安全防护
  • 7.6设计动态生成Java类

6.6理解CAS设计和由来

目录介绍

  • 01.整体的概述
    • 1.1 说一个并发案例
    • 1.2 了解CPU切片
    • 1.3 乐观锁与悲观锁
    • 1.4 高效atomic类
  • 02.CAS简单介绍
    • 2.1 什么是CAS
    • 2.2 CAS的操作过程
    • 2.3 Synchronized VS CAS
    • 2.4 CAS设计思路
    • 2.5 CAS应用场景
    • 2.6 jdk优化CAS过程
  • 03.CAS遇到的问题
    • 3.1 ABA问题如何解
    • 3.2 自旋时间过长
    • 3.3 多个变量能否安全
    • 3.4 有何局限性
  • 04.CAS底层实现原理
    • 4.1 AtomicInteger原子操作
    • 4.2 CAS考察方向
    • 4.3 Unsafe是什么
    • 4.4 底层指令如何处理
    • 4.5 知识拓展说明
  • 05.原子更新其他类型
    • 5.1 原子更新基本类型
    • 5.2 原子更新数组类型
    • 5.3 原子更新引用类型
    • 5.4 原子更新字段类型

01.整体的概述

1.1 说一个并发案例

  • 在并发编程中很容易出现并发安全的问题
    • 有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,就有可能获取不到正确的值,这是因为多线程并发导致数据存在安全问题。
    • 最常用的方法是通过Synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。
  • 这个时候可以使用AtomicInteger替代int类型数值
    • Java中提供了对应的原子操作类来实现该操作,并保证原子性,其本质是利用了CPU级别的CAS指令。由于是CPU级别的指令,其开销比需要操作系统参与的锁的开销小。
    public static class SellTicktes extends Thread {
        private static AtomicInteger integer = new AtomicInteger(100);
        @Override
        public void run() {
            // 模拟售票
            while (integer.get() != 0) {
                if (integer.get() > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //CAS乐观锁解决同步问题
                    System.out.println(Thread.currentThread().getName() + "正在出售" + integer.getAndDecrement() + "张票");
                }
            }
        }
    }
  • 今天我要问你的问题是,AtomicInteger 底层实现原理是什么?如何在自己的产品代码中应用 CAS 操作?

1.2 了解CPU切片

  • cpu的基础概念
    • cpu是时分复用的,也就是把cpu的时间片,分配给不同的thread/process轮流执行,时间片与时间片之间,需要进行cpu切换,也就是会发生进程的切换。
    • 切换涉及到清空寄存器,缓存数据。然后重新加载新的thread所需数据。当一个线程被挂起时,加入到阻塞队列,在一定的时间或条件下,在通过notify(),notifyAll()唤醒回来。
  • 什么是阻塞状态
    • 在某个资源不可用的时候,就将cpu让出,把当前等待线程切换为阻塞状态。等到资源(比如一个共享数据)可用了,那么就将线程唤醒,让他进入runnable状态等待cpu调度。

1.3 乐观锁与悲观锁

  • 悲观锁的实现
    • synchronized就是一种悲观锁,它假设最坏的情况,认为一个线程修改共享数据的时候其他线程也会修改该数据,因此只在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
  • 悲观锁的问题分析
    • 但是,由于在进程挂起和恢复执行过程中存在着很大的开销。当一个线程正在等待锁时,它不能做任何事,所以悲观锁有很大的缺点。
    • 举个例子,如果一个线程需要某个资源,但是这个资源的占用时间很短,当线程第一次抢占这个资源时,可能这个资源被占用,如果此时挂起这个线程,可能立刻就发现资源可用,然后又需要花费很长的时间重新抢占锁,时间代价就会非常的高。
  • 乐观锁的概念
    • 核心思路就是,每次不加锁而是假设修改数据之前其他线程一定不会修改,如果因为修改过产生冲突就失败就重试,直到成功为止。
    • 在上面的例子中,某个线程可以不让出cpu,而是一直while循环,如果失败就重试,直到成功为止。所以,当数据争用不严重时,乐观锁效果更好。比如CAS就是一种乐观锁思想的应用。

1.4 高效atomic类

  • 那么是否有高效的并发解决办法呢
    • 实际上,在J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。
    • atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。

02.CAS简单介绍

2.1 什么是CAS

  • 能够弄懂atomic包下这些原子操作类的实现原理,就要先明白什么是CAS操作。
  • 什么是CAS?
    • 使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。
    • 而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。
    • 所谓 CAS,表征的是一些列操作的集合,获取当前数值,进行一些运算,利用 CAS 指令试图进行更新。如果当前数值未变,代表没有其他线程进行并发修改,则成功更新。否则,可能出现不同的选择,要么进行重试,要么就返回一个成功或者失败的结果。
  • 那么,如果出现冲突了怎么办?
    • 无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

2.2 CAS的操作过程

  • 核心原理说明
    • CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。
  • CAS的操作过程
    • 当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。
    • 反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。
    • 当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程
  • CAS的实现需要硬件指令集的支撑
    • 在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。

2.3 Synchronized VS CAS

  • 两者对比分析
    • 元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。
    • 而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。

2.4 CAS设计思路

  • CAS操作通常由硬件提供支持,它可以在不使用锁的情况下实现对共享变量的原子操作。设计CAS的一般设计思路:
    • 第一步:定义共享变量:首先,您需要定义一个共享变量,它将在多个线程之间进行读取和修改。这可以是一个整数、对象引用或其他类型的变量。
    • 第二步:读取共享变量:使用CAS操作之前,需要读取共享变量的当前值。这可以通过直接读取变量的值或使用特定的读取方法来完成。
    • 第三步:比较和交换:使用CAS操作来比较共享变量的当前值与期望值,并在相等时将新值设置为共享变量的值。这个操作是原子的,即在同一时刻只有一个线程能够成功执行。
    • 第四步:处理CAS操作的结果:CAS操作返回一个布尔值,表示操作是否成功。如果操作成功,表示共享变量的值已经被修改为新值;如果操作失败,表示共享变量的值没有被修改。您可以根据返回值来确定是否成功修改了共享变量。
    • 第五步:处理CAS操作失败:如果CAS操作失败,表示有其他线程在您之前修改了共享变量的值。在这种情况下,您可以重新尝试CAS操作,或者采取其他策略,如使用自旋等待、使用锁机制或采用其他并发控制手段。

2.5 CAS应用场景

  • 在java中可以通过锁和循环CAS的方式来实现原子操作。
    • Java中java.util.concurrent.atomic包相关类就是 CAS的实现
  • atomic包里包括以下类:
    类名说明
    AtomicBoolean可以用原子方式更新的 boolean 值。
    AtomicInteger可以用原子方式更新的 int 值。
    AtomicIntegerArray可以用原子方式更新其元素的 int 数组。
    AtomicIntegerFieldUpdater<T>基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。
    AtomicLong可以用原子方式更新的 long 值。
    AtomicLongArray可以用原子方式更新其元素的 long 数组。
    AtomicLongFieldUpdater<T>基于反射的实用工具,可以对指定类的指定 volatile long 字段进行原子更新。
    AtomicMarkableReference<V>AtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。
    AtomicReference<V>可以用原子方式更新的对象引用。
    AtomicReferenceArray<E>可以用原子方式更新其元素的对象引用数组。
    AtomicReferenceFieldUpdater<T,V>基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。
    AtomicStampedReference<V>AtomicStampedReference 维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。

2.6 jdk优化CAS过程

  • 在 Java7 演变到 Java8
    • 通过以下代码我们可以看到:AtomicInteger 的 getAndSet 方法中使用了 for 循环不断重试 CAS 操作,如果长时间不成功,就会给 CPU 带来非常大的执行开销。
    • 到了 Java8,for 循环虽然被去掉了,但我们反编译 Unsafe 类时就可以发现该循环其实是被封装在了 Unsafe 类中,CPU 的执行开销依然存在。
  • LongAdder 更加高效
    • 在 JDK1.8 中,Java 提供了一个新的原子类 LongAdder。LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好,代价就是会消耗更多的内存空间。
    • LongAdder 的原理就是降低操作共享变量的并发数,也就是将对单一共享变量的操作压力分散到多个变量值上,将竞争的每个写线程的 value 值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的 value 值进行 CAS 操作,最后在读取值的时候会将原子操作的共享变量与各个分散在数组的 value 值相加,返回一个近似准确的数值。
    • 可以发现,LongAdder 在操作后的返回值只是一个近似准确的数值,但是 LongAdder 最终返回的是一个准确的数值, 所以在一些对实时性要求比较高的场景下,LongAdder 并不能取代 AtomicInteger 或 AtomicLong。

03.CAS遇到的问题

3.1 ABA问题如何解

  • ABA问题描述
    • 因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。
  • 解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。
    • 原来的变化路径A->B->A就变成了1A->2B->3A。
    • 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。通过为引用建立类似版本号(stamp)的方式,来保证 CAS 的正确性。
    • 这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

3.2 自旋时间过长

  • 自旋等待是否阻塞线程,是否涉及上下文切换
    • 自旋等待可以避免线程阻塞和上下文切换的开销。CPU时间片从一个线程切换到另一个线程时,就会发生上下文切换。
  • 自旋时间过长
    • 使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。
    • 大多数应用场景中,确实大部分重试只会发生一次就获得了成功,但是总是有意外情况,所以在有需要的时候,还是要考虑限制自旋的次数,以免过度消耗 CPU。
    • 如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。
  • 为了解决自旋等待的性能问题,可以考虑以下几种策略:
    • 策略1自适应自旋:自旋等待的时间可以根据实际情况进行调整。例如,可以根据前一次自旋等待的时间和失败次数来动态调整自旋等待的时间。
    • 策略2限制自旋次数:为了避免无限自旋,可以设置一个最大自旋次数。如果达到最大自旋次数仍然无法成功,可以放弃自旋等待,转而使用其他的并发控制机制,如锁或等待-通知机制。
    • 策略3随机自旋:在自旋等待时,可以引入一定的随机性,以避免多个线程同时自旋等待的情况。例如,可以在自旋等待时引入一个随机的短暂延迟,以使线程的自旋等待时间错开。

3.3 多个变量能否安全

  • 只能保证一个共享变量的原子操作
    • 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。
    • 比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

3.4 有何局限性

  • CAS 乐观锁在平常使用时比较受限
    • 它只能保证单个变量操作的原子性,当涉及到多个变量时,CAS 就无能为力了,但悲观锁可以通过对整个代码块加锁来做到这点。
  • 高并发写大于读的场景
    • CAS 乐观锁在高并发写大于读的场景下,大部分线程的原子操作会失败,失败后的线程将会不断重试 CAS 原子操作,这样就会导致大量线程长时间地占用 CPU 资源,给系统带来很大的性能开销。
    • 在 JDK1.8 中,Java 新增了一个原子类 LongAdder,它使用了空间换时间的方法,解决了上述问题。

04.CAS底层实现原理

4.1 AtomicInteger原子操作

  • AtomicInteger 是对 int 类型的一个封装
    • 提供原子性的访问和更新操作,其原子性操作的实现是基于 CAS (compare-and-swap) 技术。
  • 从 AtomicInteger 的内部属性可以看出,它依赖于 Unsafe 提供的一些底层能力,进行底层操作;以 volatile 的 value 字段,记录数值,以保证可见性。
    private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
    private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
    private volatile int value;
  • 具体的原子操作细节,可以参考任意一个原子更新方法,比如下面的 getAndIncrement。Unsafe 会利用 value 字段的内存地址偏移,直接完成操作。
    public final int getAndIncrement() {
        return U.getAndAddInt(this, VALUE, 1);
    }
  • 因为 getAndIncrement 需要返归数值,所以需要添加失败重试逻辑。
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }
  • 而类似 compareAndSet 这种返回 boolean 类型的函数,因为其返回值表现的就是成功与否,所以不需要重试。
    public final boolean compareAndSet(int expectedValue, int newValue)
  • CAS 是 Java 并发中所谓 lock-free 机制的基础。

4.2 CAS考察方向

  • 在开发中未必会涉及 CAS 的实现层面,但是理解其机制,掌握如何在 Java 中运用该技术,还是十分有必要的,尤其是这也是个并发编程的面试热点。
    • 有的同学反馈面试官会问 CAS 更加底层是如何实现的,这依赖于 CPU 提供的特定指令,具体根据体系结构的不同还存在着明显区别。
    • 比如,x86 CPU 提供 cmpxchg 指令;而在精简指令集的体系架构中,则通常是靠一对儿指令(如“load and reserve”和“store conditional”)实现的,在大多数处理器上 CAS 都是个非常轻量级的操作,这也是其优势所在。
    • 大部分情况下,掌握到这个程度也就够用了,没有必要让每个 Java 工程师都去了解到指令级别,我们进行抽象、分工就是为了让不同层面的开发者在开发中,可以尽量屏蔽不相关的细节。
  • 如果我作为面试官,很有可能深入考察这些方向:
    • 在什么场景下,可以采用 CAS 技术,调用 Unsafe 毕竟不是大多数场景的最好选择,有没有更加推荐的方式呢?毕竟我们掌握一个技术,cool 不是目的,更不是为了应付面试,我们还是希望能在实际产品中有价值。
    • 对 ReentrantLock、CyclicBarrier 等并发结构底层的实现技术的理解。

4.3 Unsafe是什么

4.5 知识拓展说明

  • 关于 CAS 的使用,你可以设想这样一个场景:
    • 在数据库产品中,为保证索引的一致性,一个常见的选择是,保证只有一个线程能够排他性地修改一个索引分区,如何在数据库抽象层面实现呢?
    • 可以考虑为索引分区对象添加一个逻辑上的锁,例如,以当前独占的线程 ID 作为锁的数值,然后通过原子操作设置 lock 数值,来实现加锁和释放锁,伪代码如下:
    public class AtomicBTreePartition {
    private volatile long lock;
    public void acquireLock(){}
    public void releaseeLock(){}
    }
  • 那么在 Java 代码中,我们怎么实现锁操作呢?
    • Unsafe 似乎不是个好的选择,例如,我就注意到类似 Cassandra 等产品,因为 Java 9 中移除了 Unsafe.moniterEnter()/moniterExit(),导致无法平滑升级到新的 JDK 版本。
    • 目前 Java 提供了两种公共 API,可以实现这种 CAS 操作,比如使用 java.util.concurrent.atomic.AtomicLongFieldUpdater,它是基于反射机制创建,我们需要保证类型和字段名称正确。
    private static final AtomicLongFieldUpdater<AtomicBTreePartition> lockFieldUpdater =
            AtomicLongFieldUpdater.newUpdater(AtomicBTreePartition.class, "lock");
    
    private void acquireLock(){
        long t = Thread.currentThread().getId();
        while (!lockFieldUpdater.compareAndSet(this, 0L, t)){
            // 等待一会儿,数据库操作可能比较慢
             …
        }
    }
  • Atomic 包提供了最常用的原子性数据类型,甚至是引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选。
    • 回归正题,如果是 Java 9 以后,我们完全可以采用另外一种方式实现,也就是 Variable Handle API,这是源自于JEP 193,提供了各种粒度的原子或者有序性的操作等。我将前面的代码修改为如下实现:
    private static final VarHandle HANDLE = MethodHandles.lookup().findStaticVarHandle
            (AtomicBTreePartition.class, "lock");
    
    private void acquireLock(){
        long t = Thread.currentThread().getId();
        while (!HANDLE.compareAndSet(this, 0L, t)){
            // 等待一会儿,数据库操作可能比较慢
            …
        }
    }
  • 过程非常直观,首先,获取相应的变量句柄,然后直接调用其提供的 CAS 方法。
  • 一般来说,我们进行的类似 CAS 操作,可以并且推荐使用 Variable Handle API 去实现,其提供了精细粒度的公共底层 API。
  • 我这里强调公共,是因为其 API 不会像内部 API 那样,发生不可预测的修改,这一点提供了对于未来产品维护和升级的基础保障,坦白说,很多额外工作量,都是源于我们使用了 Hack 而非 Solution 的方式解决问题。

05.原子更新其他类型

5.1 原子更新基本类型

  • atomic包提高原子更新基本类型的工具类,主要有这些:
    • 1.AtomicBoolean:以原子更新的方式更新boolean;
    • 2.AtomicInteger:以原子更新的方式更新Integer;
    • 3.AtomicLong:以原子更新的方式更新Long;
  • 这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法
    • 1.addAndGet(int delta) :以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果;
    • 2.incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果;
    • 3.getAndSet(int newValue):将实例中的值更新为新值,并返回旧值;
    • 4.getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值;
  • 可以看出,该方法实际上是调用了unsafe实例的getAndAddInt方法,unsafe实例的获取时通过UnSafe类的静态方法getUnsafe获取:
    private static final Unsafe unsafe = Unsafe.getUnsafe();
  • Unsafe类在sun.misc包下,Unsafer类提供了一些底层操作,atomic包下的原子操作类的也主要是通过Unsafe类提供的compareAndSwapInt,compareAndSwapLong等一系列提供CAS操作的方法来进行实现。
  • 下面用一个简单的例子来说明AtomicInteger的用法:
    public class AtomicDemo {
        private static AtomicInteger atomicInteger = new AtomicInteger(1);
    
        public static void main(String[] args) {
            System.out.println(atomicInteger.getAndIncrement());
            System.out.println(atomicInteger.get());
        }
    }
    输出结果:
    1
    2
  • 例子很简单,就是新建了一个atomicInteger对象,而atomicInteger的构造方法也就是传入一个基本类型数据即可,对其进行了封装。对基本变量的操作比如自增,自减,相加,更新等操作,atomicInteger也提供了相应的方法进行这些操作。但是,因为atomicInteger借助了UnSafe提供的CAS操作能够保证数据更新的时候是线程安全的,并且由于CAS是采用乐观锁策略,因此,这种数据更新的方法也具有高效性。
  • AtomicLong的实现原理和AtomicInteger一致,只不过一个针对的是long变量,一个针对的是int变量。而boolean变量的更新类AtomicBoolean类是怎样实现更新的呢?核心方法是compareAndSett方法,其源码如下:
    public final boolean compareAndSet(boolean expect, boolean update) {
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }
  • 可以看出,compareAndSet方法的实际上也是先转换成0,1的整型变量,然后是通过针对int型变量的原子更新方法compareAndSwapInt来实现的。
  • 可以看出atomic包中只提供了对boolean,int,long这三种基本类型的原子更新的方法,参考对boolean更新的方式,原子更新char,doule,float也可以采用类似的思路进行实现。

5.2 原子更新数组类型

  • atomic包下提供能原子更新数组中元素的类有:
    • 1.AtomicIntegerArray:原子更新整型数组中的元素;
    • 2.AtomicLongArray:原子更新长整型数组中的元素;
    • 3.AtomicReferenceArray:原子更新引用类型数组中的元素
  • 这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法:
    • 1.addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加;
    • 2.getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1;
    • 3.compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新
  • 可以看出,AtomicIntegerArray与AtomicInteger的方法基本一致,只不过在AtomicIntegerArray的方法中会多一个指定数组索引位i。下面举一个简单的例子:
    public class AtomicDemo {
        //    private static AtomicInteger atomicInteger = new AtomicInteger(1);
        private static int[] value = new int[]{1, 2, 3};
        private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);
    
        public static void main(String[] args) {
            //对数组中索引为1的位置的元素加5
            int result = integerArray.getAndAdd(1, 5);
            System.out.println(integerArray.get(1));
            System.out.println(result);
        }
    }
    输出结果:
    7
    2
  • 通过getAndAdd方法将位置为1的元素加5,从结果可以看出索引为1的元素变成了7,该方法返回的也是相加之前的数为2。

5.3 原子更新引用类型

  • 如果需要原子更新引用类型变量的话,为了保证线程安全,atomic也提供了相关的类:
    • 1.AtomicReference:原子更新引用类型;
    • 2.AtomicReferenceFieldUpdater:原子更新引用类型里的字段;
    • 3.AtomicMarkableReference:原子更新带有标记位的引用类型;
  • 这几个类的使用方法也是基本一样的,以AtomicReference为例,来说明这些类的基本用法。下面是一个demo
    public class AtomicDemo {
    
        private static AtomicReference<User> reference = new AtomicReference<>();
    
        public static void main(String[] args) {
            User user1 = new User("a", 1);
            reference.set(user1);
            User user2 = new User("b",2);
            User user = reference.getAndSet(user2);
            System.out.println(user);
            System.out.println(reference.get());
        }
    
        static class User {
            private String userName;
            private int age;
    
            public User(String userName, int age) {
                this.userName = userName;
                this.age = age;
            }
    
            @Override
            public String toString() {
                return "User{" +
                        "userName='" + userName + '\'' +
                        ", age=" + age +
                        '}';
            }
        }
    }
    
    输出结果:
    User{userName='a', age=1}
    User{userName='b', age=2}
  • 首先将对象User1用AtomicReference进行封装,然后调用getAndSet方法,从结果可以看出,该方法会原子更新引用的user对象,变为User{userName='b', age=2},返回的是原来的user对象User{userName='a', age=1}。

5.4 原子更新字段类型

  • 如果需要更新对象的某个字段,并在多线程的情况下,能够保证线程安全,atomic同样也提供了相应的原子操作类:

    • 1.AtomicIntegeFieldUpdater:原子更新整型字段类;
    • 2.AtomicLongFieldUpdater:原子更新长整型字段类;
    • 3.AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号。而为什么在更新的时候会带有版本号,是为了解决CAS的ABA问题;
  • 要想使用原子更新字段需要两步操作:

    • 1.原子更新字段类都是抽象类,只能通过静态方法newUpdater来创建一个更新器,并且需要设置想要更新的类和属性;
    • 2.更新类的属性必须使用public volatile进行修饰;
  • 这几个类提供的方法基本一致,以AtomicIntegerFieldUpdater为例来看看具体的使用:

    public class AtomicDemo {
    
        private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
        public static void main(String[] args) {
            User user = new User("a", 1);
            int oldValue = updater.getAndAdd(user, 5);
            System.out.println(oldValue);
            System.out.println(updater.get(user));
        }
    
        static class User {
            private String userName;
            public volatile int age;
    
            public User(String userName, int age) {
                this.userName = userName;
                this.age = age;
            }
    
            @Override
            public String toString() {
                return "User{" +
                        "userName='" + userName + '\'' +
                        ", age=" + age +
                        '}';
            }
        }
    } 
    
    输出结果:
    1
    6
  • 从示例中可以看出,创建AtomicIntegerFieldUpdater是通过它提供的静态方法进行创建,getAndAdd方法会将指定的字段加上输入的值,并且返回相加之前的值。user对象中age字段原值为1,加5之后,可以看出user对象中的age字段的值已经变成了6。

  • AtomicInteger底层实现原理是什么?如何在自己的产品代码中应用CAS操作?

    • https://time.geekbang.org/column/article/9788
贡献者: yangchong211
上一篇
6.5并发上下文切换原理
下一篇
6.7协程设计思想和原理