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

news2024/9/23 9:30:06

前言

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@ToStringclass 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 修饰的字段使用,否则会出现异常。

@Datapublic 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/69075.html

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

相关文章

第4季5:图像sensor的驱动源码的编译

以下内容源于朱有鹏嵌入式课程的学习与整理&#xff0c;如有侵权请告知删除。 本文演示如何修改sensor的驱动源码&#xff0c;修改之后如何编译与运行。 一、sensor的注册接口分析 这部分内容见第4季4&#xff1a;图像sensor的驱动源码解析。 二、黑电平 关于黑电平的概念&a…

[附源码]计算机毕业设计抗疫医疗用品销售平台Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【电巢】电源管理芯片:国产化替代厂家竞逐千亿黄金赛道

前言 整个2022年三季度&#xff0c;全国新能源电动车的起火已高达600多起&#xff0c;同比上升了30%多&#xff0c;如果具体到每天来看&#xff0c;平均每天都有超过7起新能源电动车火灾发生。 7月22日&#xff0c;台湾省专业赛车手林某颖驾驶着一辆白色特斯拉Model X&#xff…

es与mysql之间的数据同步

常见的数据同步方案有三种&#xff1a; 同步调用 异步通知 监听binlog 方案一&#xff1a;同步调用&#xff08;基本不会用&#xff0c;问题太多&#xff09; 基本步骤如下&#xff1a; hotel-demo对外提供接口&#xff0c;用来修改elasticsearch中的数据 酒店管理服务在完成…

活动预告 | DataFunSummit 2022-MLOps 及 AI 工程化落地论坛

11月19日 13&#xff1a;30-17:30&#xff0c;OpenMLDB 项目发起人、第四范式技术副总裁 郑曌作为出品人发起 DataFunSummit 2022 MLOps 及 AI 工程化落地论坛。OpenMLDB PMC、第四范式资深系统架构科师 卢冕&#xff0c;将在论坛中为大家带来议题为《开源机器学习数据库 OpenM…

吃透阿里P8推荐424页Java服务端研发知识图谱后,直接入职蚂蚁P6

前言 蓦然回首自己做开发已经十年了&#xff0c;这十年中我获得了很多&#xff0c;技术能力、培训、出国、大公司的经历&#xff0c;还有很多很好的朋友。但再仔细一想&#xff0c;这十年中我至少浪费了五年时间&#xff0c;这五年可以足够让自己成长为一个优秀的程序员&#…

看场景、重实操,实时数仓不是“纸上谈兵”

本文转载自阿里云Hologres产品负责人合一在ITPUB的访谈&#xff0c;谈谈他眼中的实时数仓&#xff0c; 原文链接&#xff1a; https://mp.weixin.qq.com/s/RZMWf9r4fKV9mNoGGUtaVw 这两年&#xff0c;企业IT领域掀起实时数仓热潮。然而&#xff0c;只要稍做梳理就会发现&#…

【笔试强训】Day 6

&#x1f308;欢迎来到笔试强训专栏 (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort目前状态&#xff1a;大三非科班啃C中&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自己的一句鸡汤&#x…

Redis - Redis为什么这么快?

1. Redis为什么这么快&#xff1f; 数据结构简单&#xff0c;对数据操作也简单&#xff0c;Redis 中的数据结构是专门进行设计的&#xff1b;基于内存实现&#xff0c;读写速度快&#xff1b;Redis 是单线程的&#xff0c;避免了不必要的上下文切换和竞争条件&#xff0c;也不…

卖座网站影院界面:优化长列表的滑动流畅度方案

关键点1&#xff1a;优化长列表的滑动流畅度 问题&#xff1a;在长列表中&#xff0c;向下滑动时&#xff0c;为了提高用户的使用感受&#xff0c;这个滑动一般都会做的很流畅。但是在网速不快的情况下&#xff0c;它自然会卡顿&#xff0c;所以为了优化网速慢带来的卡顿现象&…

好用的国产远程控制软件,我只推荐这款!

近年来&#xff0c;越来越多的人需要远程办公&#xff0c;远程为用户提供服务或支持等&#xff0c;导致人们对远程控制软件的需求不断增加。 但现在市面上远程控制软件参差不齐&#xff0c;有的远程控制软件功能不齐全&#xff0c;有的操作步骤过于繁琐&#xff0c;有的使用起…

vue 组件封装——可自由拖拽移动的盒子

最终效果 完整代码 关键性要点&#xff0c;详见注释 组件封装 superBox.vue <template><divref"box_Ref"class"box"mousedown"moveStart"mousemove"moving"mouseup"moveEnd":style"{width: (nodeInfo.width…

js模块化

第1章:模块化入门 1.1.理解什么是模块 将一个复杂的程序依据一定的规则拆分成单个文件&#xff0c;并最终组合在一起这些拆分的文件就是模块&#xff0c;模块内部数据是私有的&#xff0c;只是向外部暴露一些方法与外部其它模块通信 1.2.为什么要模块化? 降低复杂度&#…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java网络游戏虚拟交易平台8rfnp

计算机毕业设计的编程真的不会, 应该怎么办啊, 平时学了3年都没学懂&#xff0c;然而还有一个月就要答辩了&#xff0c;一点东西都没做出来&#xff0c;不知道该怎么办了&#xff1f;好迷茫&#xff0c;如果毕不到业怎么办, 有没有快速的学习方法 毕设毕设&#xff0c;最终的是…

【软件测试】10年测试老鸟,告诉你测试内功修炼之道......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 一名测试人员&#…

Docker[2]-docker的组成与卸载

1.5 Docker的基本组成 1.5.1 镜像(image) Docker 镜像&#xff08;Image&#xff09;就是一个只读的模板。镜像可以用来创建 Docker 容器&#xff0c;一个镜像可以创建很多容器。 docker面向对象容器对象镜像类 1.5.2 容器(container) Docker 利用容器&#xff08;Container&a…

一文读懂如何让网页变灰白色(黑白色)

文章目录1. 文章引言2. 实现方法2.1 修改CSS文件2.2 修改html标签2.3 修改body标签2.4 使用grayscale.js插件1. 文章引言 有段时间&#xff0c;我们打开知名的网页&#xff0c;发现全部变成黑白色了&#xff0c;如下图所示&#xff1a; 一般在清明节&#xff0c;全国哀悼日&am…

学会这些C语言技巧,你的编程能力大大提升

一、函数指针 在讲回调函数之前&#xff0c;我们需要了解函数指针。 我们都知道&#xff0c;C语言的灵魂是指针&#xff0c;我们经常使用整型指针&#xff0c;字符串指针&#xff0c;结构体指针等 int *p1; char *p2; STRUCT *p3; //STRUCT为我们定义的结构体 但是好像我们…

PCB设计总有几个阻抗没法连续的地方。

罗永浩所说“人生总有几次踩到大便的时候”&#xff0c;PCB设计也总有阻抗不能连续的时候。 特性阻抗&#xff1a;又称“特征阻抗”&#xff0c;它不是直流电阻&#xff0c;属于长线传输中的概念。在高频范围内&#xff0c;信号传输过程中&#xff0c;信号沿到达的地方&#xf…

canal springboot 入门

Alibaba canal(官方文档链接) 简介 canal [kə’nl]&#xff0c;译意为水道/管道/沟渠&#xff0c;主要用途是基于 MySQL 数据库增量日志解析&#xff0c;提供增量数据订阅和消费 早期阿里巴巴因为杭州和美国双机房部署&#xff0c;存在跨机房同步的业务需求&#xff0c;实现方…