CAS And Atomic

news2024/11/17 19:46:37

CAS(Compare And Swap 比较并交换),通常指的是这样一种原子操作:针对一个变量,首先比较它的内存值与某个期望值是否相同,如果相同,就给它赋一个新值,底层是能保证cas是原子性的

CAS的应用

在Java 中,CAS 操作是由 Unsafe 类提供支持的,该类定义了三种针对不同类型变量的 CAS 操作,对Object,Int,Long类型数据的操作,CAS可以理解为乐观锁的一种实现

Jvm会去保证可见性和有序性,

CAS源码分析:

Hotspot 虚拟机对compareAndSwapInt 方法的实现如下:

#unsafe.cpp

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jo

bject obj, jlong offset, jint e, jint x))

UnsafeWrapper("Unsafe_CompareAndSwapInt");

oop p = JNIHandles::resolve(obj);

// 根据偏移量,计算value的地址

jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);

// Atomic::cmpxchg(x, addr, e) cas逻辑x:要交换的值e:要比较的值

//cas成功,返回期望值e,等于e,此方法返回true

//cas失败,返回内存中的value值,不等于e,此方法返回false

return (jint)(Atomic::cmpxchg(x, addr, e)) == e;

UNSAFE_END

CAS缺陷

CAS 只是一个指令,不涉及用户态到内核态的切换,性能的影响是很低的,虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:

1.自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销

2.只能保证一个共享变量原子操作

3.ABA 问题

Public class AtomicIntegerTest {
    static AtomicInteger sum=new AtomicInteger(0);
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    //原子 自增 cas
                    sum.incrementAndGet();
                }
            });
            thread.start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(sum.get());

    }
}

ABA问题及其解决方案

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

ABA:当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值A修改为B,又马上将其修改为A,此时其他线程不感知,还是会修改成功。

示例:

public class ABATest {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);
        new Thread(()->{
            int value = atomicInteger.get();
            System.out.println("Thread1 read value:"+value);
            //阻塞1s
            LockSupport.parkNanos(1000000000L);
            //Thread1通过CAS修改value值为3
            if(atomicInteger.compareAndSet(value,3)){
                System.out.println("Thread1 update from "+value+"to 3");
            }else {
                System.out.println("Thread1 update fail!");
            }
        },"Thread1").start();
        new Thread(()->{
            int value=atomicInteger.get();
            System.out.println("Thread2 read value:"+value);
            if(atomicInteger.compareAndSet(value,2)){
                System.out.println("Thread2 update from"+value+"to 2");
                value=atomicInteger.get();
                System.out.println("Thread2 read value:"+value);
                if(atomicInteger.compareAndSet(value,1)){
                    System.out.println("Thread2 update from"+value+"to 1");
                }
            }else {
                System.out.println("Thread2 update fail");
            }
        },"Thread2").start();
    }
}

Thread1不清楚Thread2对value的操作,Thread1阻塞一秒期间,Thread2进行了修改操作,但最后value值都是1。

ABA问题的解决方案

数据库有个锁称为乐观锁,是一种基于数据版本实现数据同步的机制,每次修改一次数据,版本就会进行累加。同样,Java也提供了相应的原子引用类

AtomicStampedReference<V>

reference即我们实际存储的变量,stamp是版本,每次修改可以通过+1保证版本唯一性。这样就可以保证每次修改后的版本也会往上递增。

示例:

public class AtomicStampedReferenceTest {
    public static void main(String[] args) {
        //初始值 1    ,版本号 1
        //定义AtomicStampedReference Pair.reference 值为1,Pair.stamp 为1
        AtomicStampedReference atomicStampedReference = new AtomicStampedReference(1, 1);
        new Thread(() -> {
            int[] stampHolder = new int[1];
            //传入一个至少长度为1的数组 返回真实的值
            int value = (int) atomicStampedReference.get(stampHolder);
            //传入的数组 的0号位存放的是版本信息
            int stamp = stampHolder[0];
            System.out.println("Thread1 read value:" + value + ",版本 stamp:" + stamp);
            //阻塞1s
            LockSupport.parkNanos(1000000000L);
            //Thread1通过CAS修改value值为3 stamp是版本,每次修改可以通过+1保证版本唯一性
            if (atomicStampedReference.compareAndSet(value, 3, stamp, stamp + 1)) {
                System.out.println("Thread1 update from " + value + "to 3");
            } else {
                System.out.println("Thread1 update fail");
            }
        }, "Thread1").start();
        new Thread(() -> {
            int[] stampHolder = new int[1];
            int value = (int) atomicStampedReference.get(stampHolder);
            int stamp = stampHolder[0];
            System.out.println("Thread2 read value:" + value + ",stamp:" + stamp);
            //Thread2通过CAS修改value值为2
            if (atomicStampedReference.compareAndSet(value, 2, stamp, stamp + 1)) {
                System.out.println("Thread2 update from" + value + "to 2");
                value = (int) atomicStampedReference.get(stampHolder);
                stamp = stampHolder[0];
                System.out.println("Thread2 read value:" + value + ",stamp:" + stamp);
                // Thread2通过CAS修改value值为1
                if (atomicStampedReference.compareAndSet(value, 1, stamp, stamp + 1)) {
                    System.out.println("Thread2 update from" + value + "to 1");
                }
            }
        }, "Thread2").start();
    }
}

通过版本控制,最后线程1更新失败

补充:AtomicMarkableReference可以理解为上面AtomicStampedReference的简化版,就是 不关心修改过几次,仅仅关心是否修改过。因此变量mark是boolean类型,仅记录值是否有过修改。

Atomic原子操作类

基本类型:AtomicInteger、AtomicLong、AtomicBoolean;

引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference;

数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

对象属性原子修改器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、

AtomicReferenceFieldUpdater

原子类型累加器(jdk1.8增加的类):DoubleAccumulator、DoubleAdder、

LongAccumulator、LongAdder、Striped64

数组类型示例:

public class AtomicIntegerArrayTest {
    static int[] value=new int[]{1,2,3,4,5};
    static AtomicIntegerArray atomicIntegerArray=new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //设置索引0的元素为100
        atomicIntegerArray.set(0,100);
        System.out.println(atomicIntegerArray.get(0));
        //以原子更新的方式将数组中索引为1的元素与输入值相加
        atomicIntegerArray.getAndAdd(1,5);
        System.out.println(atomicIntegerArray);
    }
}

原子更新引用类型示例:

AtomicReference作用是对普通对象的封装,它可以保证你在修改对象引用时的线程安全性

public class AtomicStampedReferenceDemo {
    public static void main(String[] args) {
        User user1 = new User("张三", 23);
        User user2 = new User("李四", 25);
        User user3 = new User("王五", 20);
        //初始化为 user1
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(user1);
        //把 user2 赋给 atomicReference
        atomicReference.compareAndSet(user1, user2);
        System.out.println(atomicReference.get());
        //把 user3 赋给 atomicReference
        atomicReference.compareAndSet(user1, user3);
        System.out.println(atomicReference.get());
    }
}

LongAdder/DoubleAdder详解 累加器

AtomicLong是利用底层的CAS操作来提供并发性的,比如addAndGet方法:

上述方法内部用的CAS方式,逻辑是采用自旋的方式不断更新目标值,直到更新成功,在并发量较低的环境下,线程冲突的概率比较小,自旋的次数不会很多,但是,高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,此时AtomicLong的自旋会成为瓶颈,这就是LongAdder引入的初衷,解决高并发环境下AtomicInteger / AtomicLong的自旋瓶颈问题。

LongAdder示例:

public class LongAdderTest {
    public static void main(String[] args) {
        testAtomicLongVSLongAdder(10, 10000);
        System.out.println("==================");
        testAtomicLongVSLongAdder(10, 200000);
        System.out.println("==================");
        testAtomicLongVSLongAdder(100, 200000);
    }

    static void testAtomicLongVSLongAdder(final int threadCount, final int times) {
        try {
            long start = System.currentTimeMillis();
            testLongAdder(threadCount, times);
            long end = System.currentTimeMillis() - start;
            System.out.println("条件=====线程数:" + threadCount + ",单线程操作数" + times);
            System.out.println("结果=====LongAdder方式增加计数" + (threadCount * times) + "次,共计耗时:" + end);
            long start2 = System.currentTimeMillis();
            testAtomicLong(threadCount, times);
            long end2 = System.currentTimeMillis() - start2;
            System.out.println("条件>>>>>>线程数:" + threadCount + ", 单线程操作计数" + times);
            System.out.println("结果>>>>>>AtomicLong方式增加计数" + (threadCount * times) + "次,共计耗时:" + end2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static void testAtomicLong(final int threadCount, final int times) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        AtomicLong atomicLong = new AtomicLong();
        for (int i = 0; i < threadCount; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < times; j++) {
                        atomicLong.incrementAndGet();
                    }
                    countDownLatch.countDown();
                }
            }, "my‐thread" + i).start();
        }
        countDownLatch.await();

    }

    static void testLongAdder(final int threadCount, final int times) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        LongAdder longAdder = new LongAdder();
        for (int i = 0; i < threadCount; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < times; j++) {
                        longAdder.add(1);
                    }
                    countDownLatch.countDown();
                }
            }, "my‐thread" + i).start();
        }
        countDownLatch.await();

    }
}

低并发、一般的业务场景下AtomicLong是足够了。如果并发量很多,存在大量写多读少的情况,那LongAdder可能更合适。

LongAdder原理

AtomicLong中有个内部变量value保存着实际的long值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。

LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不

同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

LongAdder的内部结构

LongAdder内部有一个base变量,一个Cell[]数组:

base变量:非竞态条件下,直接累加到该变量上

Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中

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

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

相关文章

Android开发-AS学习(三)(布局)

相关文章链接&#xff1a;Android开发-AS学习&#xff08;一&#xff09;&#xff08;控件&#xff09;Android开发-AS学习&#xff08;二&#xff09;(控件&#xff09;Android开发应用案例——简易计算器&#xff08;附完整源码&#xff09;二、布局2.1 Linearyout常见属性说…

测试NGINX和uwsgi.ini设置

1.uwsgi修改测试 将服务器升级到16核16G配置后&#xff0c;我将uwsgi.ini中的部分参数调整如下&#xff1a; processes 32 threads 16 结果是导致内存暴满&#xff0c;然后直接服务器都无法连接&#xff0c;导致服务器卡死。之前有博客说processes处理器*2&#xff0c;结果…

【阶段三】Python机器学习26篇:机器学习项目实战:LightGBM回归模型

本篇的思维导图: 项目实战(LightGBM回归模型) 项目背景 为促进产品的销售,厂商经常会通过多个渠道投放广告。本案例将根据某公司在电视、广播和报纸上的广告投放数据预测广告收益,作为公司制定广告策略的重要参考依据。 本项目应用LightGBM回归算法进行项目实战,整…

Nginx入门

介绍&#xff1a; 下载和安装&#xff1a; 安装过程&#xff1a; 1、因为nginx是由c语言编写的&#xff0c;所以需要下载gcc进行编译 yum -y install gcc pcre-devel zlib-devel openssl openssl-devel 2、下载nginx安装包 wget https://nginx.org/download/nginx-1.16.1.ta…

【Java基础知识 1】第一个Java程序(Java的第一步)

本文已收录专栏 &#x1f332;《Java进阶之路》&#x1f332; 编写一个Java程序 第一个Java程序非常简单&#xff0c;代码如下&#xff1a; public class Java01_HelloWorld {public static void main(String[] agrs){System.out.println("欢迎来到Java进阶之路&#x…

蓝桥杯 stm32 按键点灯 CubeMX

注&#xff1a;我们使用的是 HAL 库 文章目录前言一、按键 原理图&#xff1a;二、按键CubeMX配置:三、代码讲解1. 读按键&#xff1a;&#xff08; 三行代码&#xff09;2.按键消抖&#xff1a;3&#xff0c;按键点灯&#xff1a;总结实验效果&#xff1a;前言 一、按键 原理…

基于yolov5-v7.0开发构建银行卡号实例分割检测识别分析系统

在之前的文章中我们已经做了很多基于yolov5完成实例分割的项目&#xff0c;感兴趣的话可以自行移步阅读&#xff1a;《基于YOLOv5-v7.0的药片污染、缺损裂痕实例分割检测识别分析系统》《基于yolov5-v7.0开发构建裸土实例分割检测识别模型》《基于yolov5-v7.0开发实践实例分割模…

CSDN第24期周赛(记录一下)

▶ 爱要坦荡荡 (163.com) 每一道题都有思路&#xff0c;可是只有一道Accepted100%&#xff0c;一道很简单的50%&#xff0c;一道输出错误10%并报错Segmentation Fault&#xff0c;一道字符串有思路但是没时间了 一&#xff0c;蛇皮矩阵 Accepted 10% 报错Segmentation Fault…

2、矩阵介绍

目录 一、矩阵的构造 二、矩阵大小及结构的改变 三、矩阵下标的引用 1.矩阵下标访问单个矩阵元 2.线性引用矩阵元 3.访问多个矩阵元素 四、矩阵信息的提取 1.矩阵结构 2.矩阵大小 3.矩阵的数据类型 一、矩阵的构造 矩阵的构建方式有两种&#xff0c;一种与单元数组相…

【寒假每日一题】DAY.7 有序序列判断

牛客网例题&#xff1a;点我做题 【❤️温馨提示】先做题&#xff0c;再看讲解效果更佳哟 描述 输入一个整数序列&#xff0c;判断是否是有序序列&#xff0c;有序&#xff0c;指序列中的整数从小到大排序 或者从大到小排序(相同元素也视为有序)。输入描述&#xff1a; 第一行…

【C++】stack、queue的模拟实现及deque介绍

一、stack 1. 以vector作为底层容器 从栈的接口中可以看出&#xff0c;栈实际是一种特殊的vector&#xff0c;因此使用vector完全可以模拟实现stack。 由于stack的所有工作是底层容器完成的&#xff0c;而这种具有“修改某物接口&#xff0c;形成另一种风貌”的性质&#xf…

Dubbo学习

文章目录1.概念1.1 Dubbo特性1.2 设计架构2.快速开始2.1需求假设2.2.工程架构2.3 创建模块2.3.1 gmall-interface—公共接口层2.3.2 gmall-user—用户模块2.3.3 gmall-order-web—订单模块2.3.4 测试结果2.3.5 使用Dubbo改造2.3.6 注解版3.监控中心4.整合SpringBoot5.Dubbo配置…

高性能网络设计专栏-网络编程

以下是在零声教育的听课记录。 如有侵权&#xff0c;请联系我删除。 链接&#xff1a;零声教育官网 一、网络io与select&#xff0c;poll。epoll 网络IO &#xff0c;会涉及到两个系统对象 一个是 用户空间 调用 IO 的进程或者线程&#xff0c;另一个是 内核空间的 内核系统&a…

K_A11_007 基于STM32等单片机驱动K型热电偶( MAX6675) 串口与OLED0.96双显示

[TOC](K_A11_007 基于STM32等单片机驱动K型热电偶( MAX6675) 串口与OLED0.96双显示) 一、资源说明 单片机型号测试条件模块名称代码功能STC89C52RC晶振11.0592MK型热电偶( MAX6675) 模块STC89C52RC驱动K型热电偶( MAX6675)模块串口与OLED0.96双显示STM32F103C8T6晶振8M/系统时…

后端架构学习

心理预期 1. 什么是后端服务的架构&#xff1f;怎么去理解后端架构这个词&#xff1f; 学习架构的目的&#xff1a;可以更高效的解决复杂的业务问题和技术问题。对架构设计的一知半解会导致&#xff0c;设计不足或者多度设计的现象。架构师思考问题的角度 按出发点划分 从系统…

Linux虚拟机忘记密码

Linux虚拟机忘记密码 使用虚拟机过程中&#xff0c;我们有时会忘记root的登录密码&#xff0c;我们需要进入救援模式去命令passwd更改新的密码。 编辑模式(e) 在首页&#xff0c;按住e,进入编辑模式&#xff0c;找到LANGzh_CN.UTF-8&#xff0c;在末尾加上 init/bin/sh 挂载…

学长教你学C-day10-C语言数组

“同学们&#xff0c;我们前面讲过了变量和数据类型&#xff0c;我们来复习一下&#xff0c;用C语言变量存储数据1~10&#xff0c;然后再输出。小明小红你们上黑板来写&#xff0c;其他人写纸上就可以。” 小明和小红走向讲台拿起粉笔写下&#xff1a; 小红&#xff1a; #incl…

VS操作笔记1:添加项目与查看定义

1 在解决方案中添加项目 右击解决方案 点击“添加”——“新建项目”—— 创建新项目的方法与创建第一个时一样 创建之后&#xff0c;项目结构如下&#xff1a; 现在是两个项目&#xff0c;第一个c_test项目名称字体明显加粗&#xff0c;说明这是主项目&#xff0c;编译的时…

【Go基础】切片

切片 1. 切片的定义 切片&#xff08;slice&#xff09;是Golang中独有的数据类型。 数组有特定的用处&#xff0c;但是有不足之处 &#xff1a;运行时长度不可变。切片是随处可变的&#xff0c;它构建在数组之上&#xff0c;并且提供更强大的能力和便捷。 切片&#xff08…

vite构建vue项目目录简介

文章目录1.项目目录介绍2.开发插件安装3.vue组件中的语法规范(SFC 语法规范)4.npm run dev命令执行过程1.项目目录介绍 public 下面的不会被编译 可以存放静态资源assets 下面可以存放可编译的静态资源components 下面用来存放我们的组件App.vue 是全局组件main ts 全局的ts文…