并发编程系列-CAS

锁(lock)的代价

锁是用来做并发最简单的方式,其代价也是最高的,Java在JDK1.5之前都是靠synchronized关键字来加锁。但是加锁机制会有如下几个问题:

  • 加锁、释放锁会需要操作系统进行上下文切换和调度延时,在上下文切换的时候,cpu之前缓存的指令和数据都将失效,这个过程将增加系统开销。

  • 多个线程同时竞争锁,锁竞争机制本身需要消耗系统资源。没有获取到锁的线程会被挂起直至获取锁,在线程被挂起和恢复执行的过程中也存在很大开销。

  • 等待锁的线程会阻塞,影响实际的使用体验。如果被阻塞的线程优先级高,而持有锁的线程优先级低,将会导致优先级反转(Priority Inversion)。

乐观锁与悲观锁

悲观锁:是认为别的线程会修改值。 独占锁是一种悲观锁,synchronized就是一种独占锁。synchronized加锁后就能够确保程序执行时不会被其它线程干扰,得到正确的结果。

乐观锁:本质上是乐观的,认为别的线程不会去修改值。如果发现值被修改了,可以再次重试。CAS机制(Compare And Swap)就是一种乐观锁。

Compare And Swap

CAS是一种有名的无锁(lock-free)算法。也是一种现代 CPU 广泛支持的CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。

CAS有三个操作参数:

  1. 内存位置V(它的值是我们想要去更新的)

  2. 预期原值A(上一次从内存中读取的值)

  3. 新值B(应该写入的新值)

CAS的操作过程:将内存位置V的值与A比较(compare),如果相等,则说明没有其它线程来修改过这个值,所以把内存V的的值更新成B(swap),如果不相等,说明V上的值被修改过了,不更新,而是返回当前V的值,再重新执行一次任务再继续这个过程。

所以,当多个线程尝试使用CAS同时更新同一个变量时,其中一个线程会成功更新变量的值,剩下的会失败。失败的线程可以重试或者什么也不做。

简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我这个值现在是多少”。(这段描述引自《Java并发编程实践》)

JVM对CAS的支持

在JDK1.5之前,如果不编写明确的代码就无法执行CAS操作,在JDK1.5中引入了底层的支持,在int、long和对象的引用等类型上都公开了CAS的操作,并且JVM把它们编译为底层硬件提供的最有效的方法,在运行CAS的平台上,运行时把它们编译为相应的机器指令,如果处理器/CPU不支持CAS指令,那么JVM将使用自旋锁。

在原子类变量中,如java.util.concurrent.atomic包下的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。

java.util.concurrent.atomic.AtomicLong源码中的自增getAndIncrement()方法:

    //+1操作
    public final long getAndIncrement() {
        while (true) {
            long current = get();
            long next = current + 1;
            //当+1操作成功的时候直接返回,退出此循环
            if (compareAndSet(current, next))
                return current;
        }
    }


    //调用JNI实现CAS
    public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }

JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。在jdk1.8后getAndIncrement()方法已经看不到具体代码了,而是封装在unsafe类里面。

CAS优缺点

缺点

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。

  1. ABA问题。

    因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

    从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  2. 循环时间长开销大。

    自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

  3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

  4. 比较花费CPU资源,即使没有任何争用也会做一些无用功。

  5. 会增加程序测试的复杂度,稍不注意就会出现问题。

优点

  • 可以保证变量操作的原子性;

  • 并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高;

  • 在线程对共享资源占用时间较短的情况下,使用CAS机制效率也会较高。

Java提供的CAS操作类--Unsafe类

从Java5开始引入了对CAS机制的底层的支持,在这之前需要开发人员编写相关的代码才可以实现CAS。在原子变量类Atomic**_中(例如AtomicInteger、AtomicLong)可以看到CAS操作的代码,在这里的代码都是调用了底层(核心代码调用native修饰的方法)的实现方法。在AtomicInteger源码中可以看getAndSet方法和compareAndSet方法之间的关系,compareAndSet方法调用了底层的实现,该方法可以实现与一个volatile变量的读取和写入相同的效果。在前面说到了volatile不支持例如i++这样的复合操作,在Atomic*中提供了实现该操作的方法。JVM对CAS的支持通过这些原子类(Atomic_)暴露出来,供我们使用。

而Atomic系类的类底层调用的是Unsafe类的API,Unsafe类提供了一系列的compareAndSwap*方法,下面就简单介绍下Unsafe类的API:

  • long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时使用。如下代码使用Unsafe类获取变量value在AtomicLong对象中的内存偏移。
static {
try {
    valueOffset = unsafe.objectFieldOffset
        (AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
  • int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。

  • int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素占用的字节。

  • boolean compareAndSwapLong(Object obj, long offset, long expect, long update)方法:比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false。

  • public native long getLongvolatile(Object obj, long offset)方法:获取对象obj中偏移量为offset的变量对应volatile语义的值。

  • void putLongvolatile(Object obj, long offset, long value)方法:设置obj对象中offset偏移的类型为long的field的值为value,支持volatile语义。

  • void putOrderedLong(Object obj, long offset, long value)方法:设置obj对象中offset偏移地址对应的long型field的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。

  • void park(boolean isAbsolute, long time)方法:阻塞当前线程,其中参数isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞线程会被唤醒,这个time是个相对值,是个增量值,也就是相对当前时间累加time后当前线程就会被唤醒。如果isAbsolute等于true,并且time大于0,则表示阻塞的线程到指定的时间点后会被唤醒,这里time是个绝对时间,是将某个时间点换算为ms后的值。另外,当其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时,当前线程也会返回,而当其他线程调用了unPark方法并且把当前线程作为参数时当前线程也会返回。

  • void unpark(Object thread)方法:唤醒调用park后阻塞的线程。

下面是JDK8新增的函数,这里只列出Long类型操作。

  • long getAndSetLong(Object obj, long offset, long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。
//这个方法只是封装了compareAndSwapLong的使用,不需要自己写重试机制
public final long getAndSetLong(Object var1, long var2, long var4) {
 long var6;
 do {
     var6 = this.getLongVolatile(var1, var2);
 } while(!this.compareAndSwapLong(var1, var2, var6, var4));

 return var6;
}
  • long getAndAddLong(Object obj, long offset, long addValue)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量值为原始值+addValue,原理和上面的方法类似。

总结

可以用CAS在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用CAS操作,当想要非阻塞地完成某一操作也可以考虑CAS。不推荐在复杂操作中引入CAS,会使程序可读性变差,且难以测试,同时会出现ABA问题。

顶尖架构师栈

关注回复关键字

【C01】超10G后端学习面试资源

【IDEA】最新IDEA激活工具和码及教程

【JetBrains软件名】 最新软件激活工具和码及教程

工具&码&教程

转载于:https://mp.weixin.qq.com/s/ELc_9k3QwEHdse_yFatDPw

本文由 mdnice 多平台发布

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

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

相关文章

【操作系统笔记十二】Linux常用基础命令

Linux 常用快捷键 Tab 命令或路径等的补全键,特别常用的快捷键Ctrl insert 复制命令行内容(常用可提高效率)Shift insert 粘贴命令行内容(常用可提高效率)Ctrl C 中断当前任务(退出)Ctrl Z…

备份服务器数据库并保存到Git仓库

备份项目及数据库脚本 #!/bin/bash # MySQL数据库信息 DB_HOST"localhost" DB_USER"root" DB_PASS"************" DB_NAME"my-space" # 导出文件目录 EXPORT_PATH"/home/MySpace/mysql" # 获取当前时间并格式…

ES6-解构赋值

可以将值从数组或属性从对象提取道不同的变量中。 交换变量 let a 1 let b 2 [ a, b ] [ b, a ]//a2,b1 数组 const arr [1,2,3,4]; let [a,b,c,d] arr;//a1,b2,c3,d4 let [foo] []; let [bar, foo] [1];//bar1,fooundefined 防止从数组中取出一个值为undefined的对…

逆向-beginners之结构体-成员地址访问

#include <stdio.h> #include <time.h> /* * 字节型数组 */ int main() { struct tm t; time_t unix_time; int i0, j0; unix_time time(NULL); localtime_r(&unix_time, &t); for (i 0; i < 9; i) { for (j 0; j < 4; j…

海外代理IP是什么?如何使用?

一、海外代理IP是什么&#xff1f; 首先&#xff0c;代理服务器是在用户和互联网之间提供网关的系统或路由器。它是一个服务器&#xff0c;被称为“中介”&#xff0c;因为它位于最终用户和他们在线访问的网页之间。 海外IP代理是就是指从海外地区获取的IP地址&#xff0c;用…

Zookeeper高级_四字命令

之前使用stat命令来验证ZooKeeper服务器是否启动成功&#xff0c;这里的stat命令就是ZooKeeper 中最为典型的命令之一。ZooKeeper中有很多类似的命令&#xff0c;它们的长度通常都是4个英文字母&#xff0c;因此我们称之为“四字命令”。 添加配置 vim zoo.cfg 4lw.commands…

C++之类和函数权限访问总结(二百二十七)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

window便捷使用技巧(LTS)

目前很乱&#xff0c;有空整理 %AppData%\Microsoft\Windows\Recent\AutomaticDestinations Windows应用程序设置快捷键 AutoHotkey组合键设置 热键重复调用 powertoy工具连接 Windows常用shell命令 2 台电脑共享键鼠最简单教程 QTTabBar 「资源管理器」该有的样子 win10电脑…

为什么网络安全缺口很大,而招聘却很少?学网络安全真的没有前途吗?

2020年我国网络空间安全人才数量缺口超过了140万&#xff0c;就业人数却只有10多万&#xff0c;缺口高达了93%。这里就有人会问了&#xff1a; 1、网络安全行业为什么这么缺人&#xff1f; 2、明明人才那么稀缺&#xff0c;为什么招聘时招安全的人员却没有那么多呢&#xff1…

设计模式再探——宏观篇

目录 一、背景介绍二、思路&方案三、过程1.宏观介绍2.目的与意义3.七大原则的定义与边界4.思路由来 四、总结五、升华 一、背景介绍 最近在做产品技术建模的过程中&#xff0c;一些地方刻意用到了设计模式&#xff0c;而一些地方也用到了但是并不是很明确。 于是乎就带着…

容器核心技术之Namespace与Cgroup

容器是一种流行的虚拟化技术&#xff0c;它允许我们在同一台计算机上与其他进程在独立环境中运行进程。那么容器是如何做到这一点的呢&#xff1f;为此&#xff0c;容器是从 Linux 内核的一些新功能构建的&#xff0c;其中两个主要功能是“namespace”和“cgroup”。 1.Namespa…

蓝桥杯 题库 简单 每日十题 day5

01 字符计数 字符计数 题目描述 给定一个单词&#xff0c;请计算这个单词中有多少个元音字母&#xff0c;多少个辅音字母。 元音字母包括a,e&#xff0c;i,o&#xff0c;u&#xff0c;共五个&#xff0c;其他均为辅音字母。 输入描述 输入格式&#xff1a; 输入一行&#xff0…

形式化验证方法研究综述

摘要&#xff1a;形式化验证是证明软件、硬件或系统正确性的一种方法&#xff0c;近年来受到了越来越多的关注。 本文对形式化验证的研究进行了综述。首先介绍了形式化验证的基本概念&#xff0c;然后重点介绍了形式化验证的三种技术&#xff0c;包括模型检测、定理证明和等价性…

华为华三40G带宽互通连接测试

郁闷了几天了&#xff0c;今天竟然做了件爽事&#xff01;慢慢说来。 今天下雨&#xff0c;下午娃上学&#xff0c;我送老婆去学校上课。之后到实验室&#xff0c;今年申请买的两台交换机正好送到&#xff1a; S5500V2-54S-EI&#xff1a;48个10/100/1000TX以太网端口&#x…

三维模型3DTile格式轻量化在数据存储的重要性分析

三维模型3DTile格式轻量化在数据存储的重要性分析 三维模型3DTile格式轻量化在数据存储中占有重要地位。随着科技的不断发展&#xff0c;尤其是空间信息科技的进步&#xff0c;人们对于三维地理空间数据的需求日益增长。然而&#xff0c;这类数据通常具有大尺度、高精度等特点&…

stdio.h

1.#include <corecrt.h> 1.1#include <vcruntime.h> 2.#include <corecrt_wstdio.h> 2.1#include <corecrt.h> 2.2#include <corecrt_stdio_config.h> 2.2.1#include <corecrt.h>

实例讲解Spring boot动态切换数据源

前言 在公司的系统里&#xff0c;由于数据量较大&#xff0c;所以配置了多个数据源&#xff0c;它会根据用户所在的地区去查询那一个数据库&#xff0c;这样就产生了动态切换数据源的场景。 今天&#xff0c;就模拟一下在主库查询订单信息查询不到的时候&#xff0c;切换数据…

nacos-api

登录 http://xx:8848/nacos/v1/auth/users/login?usernamexx&passwordxx 查询配置 http://xx:8848/nacos/v1/cs/configs?dataIdtest&grouptest&tenant5d24ca22-e3b0-4808-ba58-bbeeffa02195

[C++基础]-继承

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。 目录 一、模板的…

(23)认识Linq:序列与集合,延迟与缓存,链式与查询式,性能提升技巧

本文参考https://www.bilibili.com/video/BV1rx41157DS/?p6&spm_id_from333.880.my_history.page.click&vd_source2a0404a7c8f40ef37a32eed32030aa18 难度&#xff1a;中级 一、Enumerables与IEnumerable 1、什么是Enumerables? (1)Enumerabl…