通过 JDK 原子并发类 AtomicInteger 彻底掌握 CAS 无锁算法

简介: 通过 JDK 原子并发类 AtomicInteger 彻底掌握 CAS 无锁算法

在这里插入图片描述

前言

如果要聊原子类相关的话题, 可以先从基本的概念开始

1、原子类为了解决什么样的问题?

答: 为了解决并发场景下无锁的方式保证单一变量的数据一致性

2、什么情况下存在并发问题?

答: 多个线程同时读写同一个共享数据时存在多线程并发问题

解决并发安全问题的方式有很多种方式, 著名的就是 JDK 并发包 concurrent, 为了并发而存在的

非原子计算

大家应该都知道, 类似于代码中的 i++ 操作, 虽然是一行, 但是执行时候是分为三步的

  • 从主存获取变量 i
  • 变量i值+1
  • 新增后变量i值写回主存

写个小程序来更好的理解, 不论我们怎么运行下方程序, 99% 以上概率不会到达 100000000

static int NUM = 0;
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10000; i++) {
        new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                NUM++;
            }
        }).start();
    }
    Thread.sleep(2000);
    System.out.println(NUM);
    /**
     * 99149419
     */
}

可以使用 JDK 自带的 synchronized, 通过 互斥锁的方式 同步执行 NUM++ 这个代码块

static int NUM = 0;
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10000; i++) {
        new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                synchronized (Object.class) {
                    NUM++;
                }
            }
        }).start();
    }
    Thread.sleep(2000);
    System.out.println(NUM);
    /**
     * 100000000
     */
}

前面一直在说锁, 写着 AtomicInteger 在这说锁的问题, 咱不能挂着羊皮卖狗肉是吧

如果看过 JDK 源码 JUC 包下的类库源代码, 关于 Atomic 开头的类库 应该不会陌生

Atomic 英 [əˈtɒmɪk] 美 [əˈtɑːmɪk] 翻译 : 原子的

如果不使用锁来解决上面的非原子自增问题, 可以这么来写

static AtomicInteger NUM = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10000; i++) {
        new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                  // 🚩 重点哦, 自增并获取新值
                NUM.incrementAndGet();
            }
        }).start();
    }
    Thread.sleep(2000);
    System.out.println(NUM);
    /**
     * 100000000
     */
}

AtomicInteger 简介

看到一个技术点或者框架之类比较新颖的知识, 一般会思考两个问题

它是什么

AtomicInteger 是 JDK 并发包下提供的 操作 Integer 类型原子类, 通过调用底层 Unsafe 的 CAS 相关方法实现原子操作

基于乐观锁的思想实现的一种无锁化原子操作, 保障了多线程情况下单一变量的线程安全问题

关于 CAS 的概念下面会详细说明

有什么优点

AtmoicInteger 使用硬件级别的指令 CAS 来更新计数器的值, 机器直接支持的指令, 这样可以避免加锁

比如像互斥锁 synchronized 在并发比较严重情况下, 会将锁 升级到重量级锁

唤醒与阻塞线程时会有一个 用户态到内核态的一个转变, 而转换状态是需要消耗很多时间的

写过程序进行测试, 尽管 synchronized 经过升级后, 性能有了大幅度提升, 但在 一般并发场景下, CAS 无锁算法性能更高一些

当然不可能会有尽善尽美的存在, 关于 CAS 无锁算法会在下面说明劣势所在

结构分析

AtomicInteger 有两个构造方法, 分别是一个无参构造及有参构造

  • 无参构造的 value 就是 int 的默认值 0
  • 有参构造会将 value 赋值
public AtomicInteger() { }

public AtomicInteger(int initialValue) {
    value = initialValue;
}

AtomicInteger 有三个重要的变量, 分别是:

Unsafe: 可以理解它对于 Java 而言, 是一个 "BUG" 的存在, 在 AtomicInteger 里的最大作用就是直接操作内存进行值替换

value: 使用 int 类型存储 AtomicInteger 计算的值, 通过 volatile 进行修饰, 提供了内存可见性及防止指令重排序

valueOffset: value 的内存偏移量

// 获取Unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

// 静态代码块,在类加载时运行
static {
    try {
          // 获取 value 的内存偏移量
        valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) {
        throw new Error(ex);
    }
}

private volatile int value;

这里罗列出一些常用API, 核心实现思路都是一致的, 会着重讲其中一个

// 获取当前 value 值
public final int get();

// 取当前的值, 并设置新的值
public final int getAndSet(int newValue);

// 获取当前的值, 并加上预期的值
public final int getAndAdd(int delta);

// 获取当前值, 并进行自增1
public final int getAndIncrement();
  
// 获取当前值, 并进行自减1
public final int getAndDecrement();

获取当前值并自增 #getAndIncrement()

看一下具体在源码中是如何操作的

public final int getAndIncrement() {
      return unsafe.getAndAddInt(this, valueOffset, 1);
}

/**
 * unsafe.getAndAddInt
 *
 * @param var1 AtomicInteger 对象
 * @param var2 value 内存偏移量
 * @param var4 增加的值, 比如在原有值上 + 1
 * @return
 */
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 内存中 value 最新值
        var5 = this.getIntVolatile(var1, var2);
    } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

这里就是 CAS 体现无锁算法的地方, 先来说下这段代码的执行步骤

1、根据 AtomicInteger 对象 以及 value 内存偏移量获取对应 value 最新值

2、通过 compareAndSwapInt(...) 将内存中的值(var5)更改为期望的值 (var5+var4), 不存在多线程竞争成功修改返回 True 结束循环, Flase 继续执行循环

精髓的地方就在于 compareAndSwapInt(...)

/**
 * 比较 var1 的 var2 内存偏移量处的值是否和 var4 相等, 相等则更新为 var5
 *
 * @param var1 AtomicInteger 对象
 * @param var2 value 内存偏移量
 * @param var4 value 原本的值
 * @param var5 期望将 value 设置的值
 * @return
 */
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

由于是 native 关键字修饰, 我们无法查看其源码, 说明一下方法思路

1、通过 var1(AtomicInteger) 获取到 var2 (内存偏移量) 的 value 值

2、将 value(内存中值) 与 var4(线程内获取的value值) 进行比较

3、如果相等将 var5(期望值) 设置为内存中新的 value 并返回 True

4、不相等返回 False 继续尝试执行循环

图文分析 CAS

这里给出一组图片进一步理解 CAS

Unsafe#getAndAddInt() 以此方法为例

1、这个图片相当于 AtomicInteger 对象对应的 valueOffset 位置

2、线程一执行 var5 = this.getIntVolatile(var1, var2)

3、线程二执行 var5 = this.getIntVolatile(var1, var2)

此时在线程一、二的工作内存中 var5 都是 0

4、线程一欲修改内存中的 value 为1, 通过比对 var4 与内存中的值相等, 内存值成功被设置成 1

5、线程二也是要修改内存中对应的 value 值, 发现已经不相等了, 返回 False 继续尝试修改

不足之处

CAS 虽然能够实现无锁编程, 在一般情况下对性能做出了提升, 但是并不是没有局限性或缺点

1、CPU 自旋开销较大

在高并发情况下, 自旋 CAS 如果长时间不成功, 会给 CPU 带来非常大的执行开销

如果是实现高并发下的计数, 可以使用 LongAdder, 设计的高并发思想真的强!

2、著名的 "ABA" 问题

CAS 需要在操作值的时候检查下值有没有发生变化, 如果没有发生变化则更新

但是如果一个值原来是A, 变成了B, 又变成了A, 那么使用 CAS 进行检查时会发现它的值没有发生变化, 但是实际上却变化了

如果感兴趣的小伙伴可以去看下 JUCA 原子包下的 AtomicStampedReference

学习总结

到这里 CAS 以及 AtomicInteger 相关的内容就唠叨完了, 其中关于一些很细节的点由于作者无法复现, 防止误导小伙伴也没有进行说明

由于作者水平有限, 希望大家能够反馈指正文章中错误不正确的地方, 感谢 🙏

另外一点就是: 小伙伴的喜欢就是对我最大的支持 😄 , 希望大家能够关注右侧公众号支持一哈

相关文章
|
16天前
|
Java Linux
java基础(3)安装好JDK后使用javac.exe编译java文件、java.exe运行编译好的类
本文介绍了如何在安装JDK后使用`javac.exe`编译Java文件,以及使用`java.exe`运行编译好的类文件。涵盖了JDK的安装、环境变量配置、编写Java程序、使用命令行编译和运行程序的步骤,并提供了解决中文乱码的方法。
34 1
|
1月前
|
Java API 开发者
【Java字节码操控新篇章】JDK 22类文件API预览:解锁Java底层的无限可能!
【9月更文挑战第6天】JDK 22的类文件API为Java开发者们打开了一扇通往Java底层世界的大门。通过这个API,我们可以更加深入地理解Java程序的工作原理,实现更加灵活和强大的功能。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来!
|
1月前
|
Java API 开发者
【Java字节码的掌控者】JDK 22类文件API:解锁Java深层次的奥秘,赋能开发者无限可能!
【9月更文挑战第8天】JDK 22类文件API的引入,为Java开发者们打开了一扇通往Java字节码操控新世界的大门。通过这个API,我们可以更加深入地理解Java程序的底层行为,实现更加高效、可靠和创新的Java应用。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来,并积极探索类文件API带来的无限可能!
|
2月前
|
搜索推荐 算法 Java
现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法
该博客文章通过UML类图和Java源码示例,展示了如何使用适配器模式将QuickSort类和BinarySearch类的排序和查找功能适配到DataOperation接口中,实现算法的解耦和复用。
23 1
现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法
|
2月前
|
存储 Java
【Java集合类面试七】、 JDK7和JDK8中的HashMap有什么区别?
JDK7中的HashMap使用数组加链表解决冲突,而JDK8增加了红黑树结构以优化链表过长时的性能,提高查找效率。
|
2月前
|
Oracle 安全 Java
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
|
2月前
|
Java 编译器 API
JDK版本特性问题之在 JDK 17 中,想声明一个密封类,如何解决
JDK版本特性问题之在 JDK 17 中,想声明一个密封类,如何解决
|
2月前
|
数据采集 算法 数据可视化
基于K-Means聚类算法对球员数据的聚类分析,可以自主寻找最优聚类数进行聚类
本文介绍了一个基于K-Means聚类算法的NBA球员数据分析项目,该项目通过采集和分析球员的得分、篮板、助攻等统计数据,使用轮廓系数法和拐点法确定最优聚类数,将球员分为不同群组,并提供了一个可视化界面以便直观比较不同群组的球员表现。
基于K-Means聚类算法对球员数据的聚类分析,可以自主寻找最优聚类数进行聚类
|
3月前
创建KNN类
【7月更文挑战第22天】创建KNN类。
28 8
|
2月前
|
Java API
JDK版本特性问题之Hidden Classes(隐藏类)在 JDK 15 中有何用途,如何实现
JDK版本特性问题之Hidden Classes(隐藏类)在 JDK 15 中有何用途,如何实现