浅析CAS

news2024/10/7 20:34:07

CAS基本使用

以ReentrantLock为例,观察CAS基本使用。

class ReentrantLockExample {
    int a = 0;
    // 非公平锁
    ReentrantLock lock = new ReentrantLock(false);

    public void writer() {
        // 获取锁
        lock.lock();
        try {
            a++;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    public void reader() {
        // 获取锁
        lock.lock();
        try {
            int i = a;
            // ...
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS)AQS使用一个整型的volatile变量(命名为state)来维护同步状态,这个volatile变量是ReentrantLock内存语义实现的关键。

ReentrantLock类图

image-20230711221446173

使用公平锁时,加锁方法lock()调用轨迹如下:

  1. ReentrantLock:lock()
  2. FairSync:lock()
  3. AbstractQueuedSynchronizer:acquire(int arg)
  4. ReentrantLock:tryAcquire(int acquires)

在第4步真正开始加锁,源码如下:

        protected final boolean tryAcquire(int acquires) { // acquires 调用时值为1
            final Thread current = Thread.currentThread();
            // 加锁前,先获取锁,读volatile变量(state初始值为0
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) { // CAS比较并交换
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

lock()加锁前首先读volatile变量state。

在使用公平锁时,解锁方法unlock()调用轨迹如下:

  1. ReentrantLock:unlock()
  2. AbstractQueuedSynchronizer:release(int arg)
  3. Sync:tryRelease(int releases)

在第3步真正开始释放锁,源码如下:

        protected final boolean tryRelease(int releases) { // 调用时值为1
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 此时加完锁,state为1,1 - 1 = 0 nextc = 0
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            // 释放锁的最后,写volatile变量state,恢复为0
            setState(nextc);
            return free;
        }

unlock()在释放锁的最后写volatile变量state。

非公平锁的释放和公平锁完全一样,仅仅分析非公平锁的获取。使用非公平锁时,加锁方法lock()调用轨迹如下:

  1. ReentrantLock:lock()
  2. NonfairSync:lock()
  3. AbstractQueuedSynchronizer:compareAndSetState(int expect, int update)

image-20230711223510983

在第3步真正开始加锁,源码如下:

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

该方法以原子操作的方式更新state变量(CAS)。

原理解析

JDK文档对上述方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有volatile读和写的内存语义。

编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序(插入内存屏障)。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。

下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

是一个本地方法调用。这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomic_windows_x86.inline.hpp。

这个本地方法的最终实现在openjdk的如下位置:openjdk\hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp(对应于Windows操作系统,X86处理器)。intel X86处理器的源代码片段:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, 
        jint     compare_value) {
     // alternative for InterlockedCompareExchange
     int mp = os::is_MP();
     __asm {
       mov edx, dest
       mov ecx, exchange_value
       mov eax, compare_value
       LOCK_IF_MP(mp)
       cmpxchg dword ptr [edx], ecx
    }
}

程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(Lock Cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

intel的手册对lock前缀的说明

  1. 确保对内存的读-改-写操作原子执行。Intel使用缓存锁定(Cache Locking)来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。(某些处理器可能会锁总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。)
  2. 禁止该指令,与之前和之后的读和写指令重排序。
  3. 把写缓冲区中的所有数据刷新到内存中。

第2点和第3点所具有的内存屏障效果,足以让CAS同时实现volatile读和volatile写的内存语义。

公平锁和非公平锁的内存语义总结:

  • 公平锁和非公平锁释放时,最后都要写一个volatile变量state。
  • 公平锁获取时,首先会去读volatile变量。
  • 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

对ReentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方式:

  1. 利用volatile变量的写-读所具有的内存语义。
  2. 利用CAS所附带的volatile读和volatile写的内存语义。

CAS三大问题

JVM中的CAS操作利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

线程安全(原子类)/ 不安全 i++操作:

public class Counter {
    private final AtomicInteger atomicI = new AtomicInteger(0);
    private int i = 0;

    public static void main(String[] args) {
        final Counter cas = new Counter();
        List<Thread> ts = new ArrayList<>(600);
        long start = System.currentTimeMillis();
        for (int j = 0; j < 100; j++) {
            // 创建100个线程
            Thread t = new Thread(() -> {
                // 每个线程 数字 + 10000
                for (int i = 0; i < 10000; i++) {
                    cas.count();
                    cas.safeCount();
                }
            });
            ts.add(t);
        }
        for (Thread t : ts) {
            t.start();
        }
        // 等待所有线程执行完成
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 期望 1000000
        System.out.println(cas.i);
        System.out.println(cas.atomicI.get());
        System.out.println("consume time " +(System.currentTimeMillis() - start) + "ms");
    }

    /**
     * 使用CAS实现线程安全计数器
     */
    private void safeCount() {
//        for (; ; ) {
//            int i = atomicI.get();
//            boolean suc = atomicI.compareAndSet(i, ++i);
//            if (suc) {
//                break;
//            }
//        }

        // 两种都可以
        atomicI.incrementAndGet();
    }

    /**
     * 非线程安全计数器
     */
    private void count() {
        i++;
    }
}

测试结果:

image-20230706002222241

原子类可以保证线程安全操作。原子类虽好但也存在问题。

ABA问题

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

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题

该类compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(
               V   expectedReference,        // 预期引用
               V   newReference,             // 更新后的引用
               int expectedStamp,            // 预期标志
               int newStamp                  // 更新后的标志
)

循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。

pause指令有两个作用

第一延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;

第二避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率

相关CPU术语:

image-20230706000408658

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

从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

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

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

相关文章

生产环境 kafka 平滑迁移之旅

文章目录 背景分析测试环境验证现实很残酷两种抉择-----leader分区切换方案选择实施步骤手工副本集增加步骤手工leader分区切换步骤 总结 背景 线上kafka集群&#xff0c;3台机器&#xff0c;3个broker&#xff1b;其中某台机器因为硬件故障&#xff0c;需要停机维修&#xff…

MSP432学习笔记12:MSP432时钟源与定时器A时钟源配置

今日深入学习一下MSP432的时钟源与配置&#xff0c; 可以结合之前的滴答计时器相关文章&#xff1a; MSP432学习笔记4&#xff1a;时钟与滴答计时器_NULL指向我的博客-CSDN博客 目录 MSP432有关时钟源系统的性能&#xff1a; 七种时钟源&#xff1a; 五种时钟&#xff1a; …

创新实践,复合机器人采摘运输教育沙盘案例研究

引言 在之前我们已经介绍了水果采摘和分拣机器人的应用场景&#xff0c;今天我们来介绍复合机器人水果采摘运输的场景。 作为最热门的技术领域&#xff0c;机器人技术正在彻底改变各行各业&#xff0c;推动全球创新。为了满足这一快速发展领域对专业技术人才日益增长的需求&a…

【Matlab】智能优化算法_广义正态分布优化算法GNDO

【Matlab】智能优化算法_广义正态分布优化算法GNDO 1.背景介绍2.数学模型2.1 局部开采2.2 全局勘探 3.文件结构4.伪代码5.参考文献 1.背景介绍 GNDO受到正态分布理论的启发。正态分布也称为高斯分布&#xff0c;是描述自然现象的一个非常重要的工具。正态分布可以定义如下。假设…

NLP Transformer的Decoder的输入输出都是什么?能解释一下每个部分都是什么?

要弄清楚Decoder的输入输出&#xff0c;关键在于图示三个箭头的位置&#xff1a; 以翻译为例&#xff1a; 输入&#xff1a;我爱中国输出&#xff1a; I Love China 因为输入&#xff08;“我爱中国”&#xff09;在Encoder中进行了编码&#xff0c;这里我们具体讨论Decoder的…

【工具推荐】企业微信、企业飞书接口调用工具

github地址: GitHub - fasnow/idebug: 企业微信、企业飞书接口调用工具。 简介 企业微信、企业飞书接口调用工具。 使用方法 wechat模块 使用use wechat 选择模块。 首先设置corpid和corpsecret&#xff0c;如有需要可以设置代理&#xff0c;之后再执行run命令。 导出通信…

飞行动力学 - 第6节-part3-风对航程的影响 之 基础点摘要

飞行动力学 - 第6节-part3-风对航程的影响 之 基础点摘要 1. 风对航程的影响2. 典型飞机航程3. 世界上最长航线4. 参考资料 1. 风对航程的影响 可以认为风移动的距离相当于飞机在静止空间移动的距离加上风移动的距离。 在物理上可以简单的理解为两个矢量叠加和。 回顾喷气式&…

WebGPU实战3D电商

在过去的几年里&#xff0c;我们一直在为 WebGPU 编写新版本的 Babylon.js 引擎。 随着下一代 Web 3D 即将在Chrome 102~103版本上公开WebGPU 1.0 &#xff0c;人们的兴奋情绪与日俱增。 在这篇博文中&#xff0c;我将快速概述这个新的 Babylon.js WebGPU 引擎&#xff0c;并将…

100种思维模型之安全边际思维模型-92

安全边际&#xff0c; 简而言之即距离某一件糟糕的事件发生&#xff0c;还有多大的空间&#xff0c;安全边际越高&#xff0c;我们就越安全&#xff01; 安全边际思维模型一个 让生活变得更从容 的 思维模型。 01、何谓安全边际思维模型 一、安全边际思维 安全边际 源于…

千云探探监测到7月4日法国Facebook社交网络异常

针对法国近期出现的骚乱游行&#xff0c;法国司法部长莫雷蒂7月1日时候表示&#xff0c;法国检察官要求互联网运营商提供在社交平台Snapchat上号召骚乱年轻人的IP地址。 法国总统马克龙7月4日表示&#xff1a;“如果事态失控&#xff0c;我们可能需要监管或关闭它们&#xff0…

idea 添加类库

打开项目中的独立环境文件夹&#xff0c;右键打开终端输入安装类库的命令&#xff1a; pip install requests pip3 install BeautifulSoup4 检查这里是否把类库加进来了&#xff0c;加进来就完成&#xff01;

第十二章 kafka

Producer:Producer即生产者,消息的产生者,是消息的入口。 kafka cluster: Broker:Broker是kafka实例,每个服务器上有一个或多个kafka的实例,我们姑且认为每个broker对应一台服务器。每个kafka集群内的broker都有一个不重复的编号,如图中的broker-0、broker-1等…… 主…

Windows如何恢复已删除的Word文档?

案例&#xff1a;可以恢复已删除的Word文档吗&#xff1f; “大家好&#xff0c;我遇到了一个问题&#xff0c;需要大家的帮助。昨天我编辑了一个Word文档并保存到了桌面上&#xff0c;但当我今天再次打开电脑时&#xff0c;它就不见了&#xff01;昨天工作完成后&#xff…

mysql数据库以及管理流程

目录 1.基本概念 2.DBMS工作模式 3.关系型数据库和非关系型数据库 4.数据库管理 sql语句 5.一些命令 6.增删改查命令 DDL DML 7.案例 创建表 删除 DML管理表中内容 增加内容 改内容 删内容 DCL具体应用 1.基本概念 1.数据 描述事物的符号记录(数字 文字 图像等) …

【报错记录】解决CentOS免密失败的问题,以及解决免密问题的排查流程

前言 本文相当于对之前的文章进行的补充【原创】三台CentOS7非root用户间实现相互间的免密登录_DCTANT的博客-CSDN博客 现场遇到一台旧服务器与其他服务器免密失败的问题&#xff0c;明明.ssh目录中authorized_keys中的公钥设置都是正确的&#xff0c;但是别的服务器连它都得…

基础篇--STM32原理图设计

学会查看数据手册 芯片数据手册获取方式 ST官网&#xff1a;https://www.st.comST中文社区网&#xff1a;https://www.stmcu.org.cn/ 数据手册内容概要 芯片的基本参数&#xff08;STM32F103ZET6为例&#xff09; 主频/FLASH/SRAM &#xff1a; 72MHz/512KB/64KB工作电压/…

vue3 + axios 实现带进度条的下载对话框

文章目录 问题实现采用axios实现下载请求写一个进度下载对话框调用对话框 参考链接 问题 上传下载是前端经常面临的两大需求&#xff0c;当文件比较大时&#xff0c;下载进度显示能提升用户体验。本文结合vue3介绍下载对话框的实现。当点击页面中下载按钮后&#xff0c;会呈现…

在日常学习生活中,究竟该如何保持稳定的情绪呢?

方向一&#xff1a;分享工作中让你有强烈情绪波动的事情 编程仅仅是工程的过程实践化&#xff0c;投入进去确实会出现精神集中&#xff0c;出现问题排查过程会绞尽脑汁&#xff0c;甚至抓耳挠腮&#xff0c;当发现问题无法解决时不如放下来想一想&#xff0c;是不是知识点掌握不…

按日,周,年统计,无的数据补充0

需求&#xff1a;按日-周-年统计。统计涉及到3张表数据。 写sql。先把3张表数据摘取出来&#xff0c;只需对3张表的时间做分组统计即可。 按日统计 select DAY(dateff) as time,IFNULL(count(id),0)as num from(select create_time as dateff,id as id from cz_taxi_orders…

go语言开发 三种容器类型:数组、切片、map

目录 go语言中的三种容器类型&#xff1a;数组&#xff0c;切片&#xff0c;map go 与 python的容器类型对比 python go&#xff1a; 数组&#xff1a; 数组的定义&#xff1a;&#xff08;定义数组的格式、二维数组、初始化、任意长度初始化、取值&#xff09; 数组的…