盘点JDK中基于CAS实现的原子类

news2024/9/22 23:21:13

前言

JDK中提供了一系列的基于CAS实现的原子类,CAS 的全称是Compare-And-Swap,底层是lock cmpxchg指令,可以在单核和多核 CPU 下都能够保证比较交换的原子性。所以说,这些原子类都是线程安全的,而且是无锁并发,线程不会频繁上下文切换,所以在某些场景下性能是优于加锁。

本文就盘点一下JDK中的原子类,方便我们后续拿来使用。

基础原子类

  • AtomicInteger:Integer整数类型的原子操作类
  • AtomicBoolean:Boolean类型的原子操作类
  • AtomicLong:Long类型的原子操作类

这边以AtomicInteger讲解下它的API和用法。

构造方法:

  • public AtomicInteger():初始化一个默认值为 0 的原子型 Integer
  • public AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer

常用API:

  • public final int get(): 获取 AtomicInteger 的值
  • public final int getAndIncrement(): 以原子方式将当前值加 1,返回的是自增前的值
  • public final int incrementAndGet():以原子方式将当前值加 1,返回的是自增后的值
  • public final int getAndSet(int value):以原子方式设置为 newValue 的值,返回旧值
  • public final int addAndGet(int data):以原子方式将输入的数值与实例中的值相加并返回

使用:

  • 结果1000,大致说明并发情况下保证了线程安全

原理分析:

整体实现思路: 自旋(循环) + CAS算法

  • 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
  • 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋
public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
 }
复制代码
  • valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据

  • 从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 compareAndSwapInt() 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据。

  • 原子类AtomicIntegervalue属性是volatile类型,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量。

原子引用

原子引用主要是对对象的原子操作,原子引用类分为AtomicReferenceAtomicStampedReferenceAtomicMarkableReference。它们之间有什么区别呢?

  1. AtomicReference类

普通的原子类对象

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User user1 = new User("旭阳");

        // 创建原子引用包装类
        AtomicReference<User> atomicReference = new AtomicReference<>(user1);

        while (true) {
            User user2 = new User("alvin");
            // 比较并交换
            if (atomicReference.compareAndSet(user1, user2)) {
                break;
            }
        }
        System.out.println(atomicReference.get());
    }
}

@Data
@AllArgsConstructor
@ToString
class User {
    private String name;
}
复制代码
  • 调用compareAndSet()方法进行比较替换对象

ABA问题

但是如果使用AtomicReference类,会有一个ABA问题。什么意思呢?就是一个线程将共享变量从A改成B, 后面又改回A, 这是,另外一个线程就无法感知这个变化过程,就傻傻的比较,就以为没有变化,还是一开始的A,就替换了。 实际的确存在这样只要共享变量发生过变化,就要CAS失败,有什么办法呢?

  1. AtomicStampedReference类

带版本号的原子类对象

@Slf4j(topic = "a.AtomicStampedReferenceTest")
public class AtomicStampedReferenceTest {
    // 构造AtomicStampedReference
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);
        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();
        Thread.sleep(1000);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }
    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t1").start();

        Thread.sleep(500);

        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t2").start();
    }
}
复制代码

  • 虽然对象的值变回了A,但是由于版本变了,所以主线程CAS失败
  1. AtomicMarkableReference 类

其实有时候并不关心共享变量修改了几次,而是只要标记下是否发生过更改,是否加个标记即可,所以就有了AtomicMarkableReference类。

@Slf4j(topic = "c.AtomicMarkableReferenceTest")
public class AtomicMarkableReferenceTest {
    // 构造 AtomicMarkableReference, 初始标记为false
    static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        other();
        Thread.sleep(1000);
        // 看看是否发生了变化
        log.debug("change {}", ref.isMarked());
    }
    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    false, true));
        }, "t1").start();

        Thread.sleep(500);

        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    true, true));
        }, "t2").start();
    }
}
复制代码

  • 通过调用isMarked()方法查看是否发生变化。

原子数组

  • AtomicIntegerArray: Integer类型的原子数组
  • AtomicLongArray:Long类型的原子数组
  • AtomicReferenceArray:引用类型的原子数组

直接上例子

public class AtomicIntegerArrayTest {
    public static void main(String[] args) throws Exception{
        AtomicIntegerArray array = new AtomicIntegerArray(10);
        Thread t1 = new Thread(()->{
            int index;
            for(int i=1; i<100000; i++) {
                index = i%10; //范围0~9
                array.incrementAndGet(index);
            }
        });

        Thread t2 = new Thread(()->{
            int index;
            for(int i=1; i<100000; i++) {
                index = i%10; //范围0~9
                array.decrementAndGet(index);
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(5 * 1000);
        System.out.println(array.toString());
    }
}
复制代码

  • 两个线程同时对数组对象进行加和减的操作,最终结果都是0,说明线程安全。

原子字段更新器

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。

@Data
public class AtomicReferenceFieldUpdaterTest {

    private volatile int age = 10;

    private int age2;

    public static void main(String[] args) {
        AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age");

        AtomicReferenceFieldUpdaterTest ref = new AtomicReferenceFieldUpdaterTest();
        // 对volatile 的age字段+1
        integerFieldUpdater.getAndIncrement(ref);
        System.out.println(ref.getAge());

        // 修改 非volatile的age2
        integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age2");
        integerFieldUpdater.getAndIncrement(ref);
    }
}
复制代码

  • 原子字段更新器只能更新volatile字段,它可以保证可见性,但是无法保证原子性。

原子累加器

原子累加器主要是用来做累加的,相关的类有LongAdderDoubleAdderLongAccumulatorDoubleAccumulator

LongAdder是jdk1.8中引入的,它的性能要比AtomicLong方式好。

LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr

这边做个性能的对比例子。

public class LongAdderTest {

    public static void main(String[] args) {
        System.out.println("LongAdder ...........");
        for (int i = 0; i < 5; i++) {
            addFunc(() -> new LongAdder(), adder -> adder.increment());
        }
        System.out.println("AtomicLong ...........");
        for (int i = 0; i < 5; i++) {
            addFunc(() -> new AtomicLong(), adder -> adder.getAndIncrement());
        }
    }

    private static <T> void addFunc(Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>();

        // 40个线程,每人累加 50 万
        for (int i = 0; i < 40; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start)/1000_000);
    }
}
复制代码

主要是由于LongAdder会设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

总结

本文总结了JDK中提供的各种原子类,包括基础原子类、原子引用类、原子数组类、原子字段更新器和原子累加器等。有时候,使用这些原子类的性能是比加锁要高的,特别是在读多写少的场景下。但是,不知道大家发现没有,所有的原子类操作对于一个共享变量执行操作是原子的,如果对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,还是老老实实加锁吧。

如果本文对你有帮助的话,请留下一个赞吧

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/67441.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

国内设计师经常逛的5个素材网站

设计师必备的5个设计素材网站&#xff0c;免费下载&#xff0c;还可商用&#xff0c;再也不用担心侵权了。1、菜鸟图库 https://www.sucai999.com/?vNTYwNDUx菜鸟图库是一个素材量非常丰富的网站&#xff0c;网站聚合了平面、UI、淘宝电商、高清背景图、图片、插画等高质量素材…

六 游戏基础知识和SHAPE

显示对象 在Egret渲染架构设计中&#xff0c;我们将能够参与渲染的对象抽象为显示对象 Egret引擎中所有的显示对象类型表格&#xff1a; 坐标系 egret游戏中的的坐标原点位于舞台的左上角 显示列表与容器类 所有的容器全部继承于 DisplayObjectContainer类 DisplayObje…

世界杯主题系列-用Scratch制作足球比赛小游戏,源码分享啦

目录 一&#xff1a;两个小游戏的预览图效果图&#xff1a; 二&#xff1a;简单版双人足球赛源码&#xff1a; 三&#xff1a;复杂多人控制版足球赛源码 先来动态gif效果图&#xff1a; 四年一度的世界杯正在卡塔尔火热进行中&#xff01; 在绿茵场内&#xff0c;有胜利的…

Reids实战——分布式锁优化(Lua脚本)

1 基于Redis分布式锁的问题 先来看看之前分布式锁的实现。 这个基于Redis的分布式锁仍然有着一个问题&#xff0c;那就是误删锁的问题。 简单的来说&#xff0c;就是当第一个线程&#xff0c;也就是线程1&#xff0c;拿到锁后&#xff0c;但由于本身业务复杂&#xff0c;而导…

gvim写verilog环境搭建——将文本编辑器客制化定义为你自己的IDE

本文将详细描述将vim变成写Verilog代码的IDE客制化的实现过程&#xff0c;同时最后提供自己的环境。实现的过程中有使用自己写的&#xff0c;也有参考借鉴其他同学进行一定的修改&#xff0c;也有直接使用插件实现的相关功能。对应的功能实现部分均会给出相应的参考与插件地址等…

基于jsp+ssm的网上图书商城-计算机毕业设计

项目介绍 本系统的用户群体分为管理员、会员两类。不同的身份拥有不同的职责和权限。管理员的职能包括&#xff1a;图书管理、会员管理、订单信息审核、个人信息维护等 该网上书城系统应该具备有图书添加、图书信息修改、用户浏览图书、用户搜索图书、图书购买和订购等功能&a…

为什么在高速PCB设计当中信号线不能多次换孔

大家在进行PCB设计时过孔肯定是要接触的&#xff0c;那么大家知道过孔对于我们PCB的信号质量影响有多大吗&#xff1f; 在搞清楚上面这个这个问题之前我们先给大家介绍一下我们在PCB设计时过孔应该如何选取。 一般过孔种类有以下三种可以进行选择&#xff1a;&#xff08;单位是…

00后会不会改变软件测试行业现状?

不可置否&#xff0c;从年开始&#xff0c;00后开始进入职场&#xff0c;开始他们漫长的职业生涯啦。 作为经历过90后改变职场规则的80后的老人们&#xff0c;又要瑟瑟发抖啦。 因为在90后之后&#xff0c;00后又开始整顿职场了。 80后&#xff0c;上有老下有小&#xff0c;为…

纪念DedeCMS创始人IT柏拉图先生

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; IT柏拉图开发了DedeCMS&#xff0c;造福了千万站长&#xff0c;但却没有因为这套系统过上体面的生活。 &#xff08;图片取自IT柏拉图的新浪微博&#xff09; 1979年你出生了&#xff0c;比我大…

【刷题心得】双指针法|HashSet<T>

题目 1 概念梳理 1.1 ISet<T> HashSet<T> 包含不重复元素的集合称为“集set”。.NET Framework包含两个集HashSet<T>和SortedSet<T>&#xff0c;其中HashSet<T>包含不重复元素的无序列表&#xff0c;SortedSet<T>包含不重复元素的有序列…

数据科学必备Python编程基础

前言 对于Python复杂的编程语言中提取了数据分析常用的数据处理以及数据可视化等数据分析师常用的内容&#xff0c;区别与其他的Python编程教程&#xff0c;如果是纯开发的小伙伴&#xff0c;看完本系列的文章仅仅只能掌握数据相关处理的内容&#xff0c;并不能完全掌握开发方…

RestoreDet

来说说低分辨率图像中目标检测吧~~ 超分辨率&#xff08;SR&#xff09;等图像恢复算法是退化图像中目标检测不可或缺的预处理模块。然而&#xff0c;这些算法中的大多数假设退化是固定的并且是先验已知的。 论文地址&#xff1a;https://arxiv.org/pdf/2201.02314.pdf 当真…

四、文件管理(三)文件系统

目录 3.1文件系统结构 3.2文件系统布局 3.2.1文件系统在外存中的结构 3.2.2文件系统在内存中的结构 3.3外存空闲空间管理 3.3.1空闲表法 3.3.2空闲链表法 3.3.3位示图法 3.3.4成组链接法 3.4虚拟文件系统 3.5文件系统挂载(mounting) 3.1文件系统结构 从不同的角度出…

柚子是肝病“催化剂”吗?医生:滋养肝细胞,5种水果或可常吃

人体就像是一台大型的机器&#xff0c;五脏六腑就像是机器里的重要组成部分&#xff0c;血管是为五脏六腑输送机油的管道&#xff0c;骨骼是机器的整体框架等&#xff0c;它们彼此之间互相配合&#xff0c;机器才能够正常运转。 如果其中一个构造受损了&#xff0c;那么会影响到…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java散酒营销系统w5at6

对于计算机专业的学生最头疼的就是临近毕业时的毕业设计,对于如何选题,技术选型等这些问题,难道了大部分人,确实,还没毕业的学生对于这些问题还比较陌生,只有学习的理论知识,没有实战经验怎么能独自完成毕业设计这一系列的流程,今天我们就聊聊如何快速应对这一难题. 比较容易的…

第二证券|元宇宙赛道西北首个“吃螃蟹者”入场

元世界展开势头一浪高过一浪之际&#xff0c;地处西北的咸阳高新区也下决心参加这场比赛。 近来&#xff0c;咸阳高新区管委会发布《咸阳高新区元世界工业先行区举动方案》&#xff0c;对行将打出的元世界“先手牌”从多个维度予以规划。 值得关注的是&#xff0c;外界印象中&…

iOS视频流采集概述(AVCaptureSession)

需求&#xff1a;需要采集到视频帧数据从而可以进行一系列处理(如: 裁剪&#xff0c;旋转&#xff0c;美颜&#xff0c;特效....). 所以,必须采集到视频帧数据. 阅读前提: 使用AVFoundation框架 采集音视频帧数据 Overview AVCaptureSession:使用相机或麦克风实时采集音视频…

Java编程实战12:解数独

目录解数独题目示例 1提示解答解题思路完整代码解数独 题目 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能…

1553_AURIX_TC275_CCU寄存器以及模块的独立时钟生成

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 这里的BBB时钟应该是前面看到的调试时钟&#xff0c;BBB比较醒目&#xff0c;不过暂且不知道这个缩写是什么词语的缩写。 这个寄存器是CPU0的时钟设置寄存器。我有点好奇&#xff0c;这个寄…

多线程并发、线程池、同步方法、同步代码块(锁)

多线程&#xff1a; 同时执行多个程序 多线程需要硬件支持&#xff08;cpu&#xff09; 并行&#xff1a;在同一时刻&#xff0c;有多个指令在多个cpu上同时执行 并发&#xff1a;在同一时刻&#xff0c;有多个指令在单个cpu上交替执行 进程&#xff1a;正在运行的软件 独立…