引言

传统的并发控制手段,如使用synchronized关键字或者ReentrantLock等互斥锁机制,虽然能够有效防止资源的竞争冲突,但也可能带来额外的性能开销,如上下文切换、锁竞争导致的线程阻塞等。而此时就出现了一种乐观锁的策略,以其非阻塞、轻量级的特点,在某些场合下能更好地提升并发性能,其中最为关键的技术便是Compare And Swap(简称CAS)。

关于synchronize的实现原理,请看移步这篇文章:美团一面:说说synchronized的实现原理?问麻了。。。。

关于synchronize的锁升级,请移步这篇文章:京东二面:Sychronized的锁升级过程是怎样的?

CAS是一种无锁算法,它在硬件级别提供了原子性的条件更新操作,允许线程在不加锁的情况下实现对共享变量的修改。在Java中,CAS机制被广泛应用于java.util.concurrent.atomic包下的原子类以及高级并发工具类如AbstractQueuedSynchronizer(AQS)的实现中。

CAS的基本概念与原理

CAS是一种原子指令,常用于多线程环境中的无锁算法。CAS操作包含三个基本操作数:内存位置、期望值和新值。在执行CAS操作时,计算机会检查内存位置当前是否存放着期望值,如果是,则将内存位置的值更新为新值;若不是,则不做任何修改,保持原有值不变,并返回当前内存位置的实际值。

在Java中,CAS机制被封装在jdk.internal.misc.Unsafe类中,尽管这个类并不建议在普通应用程序中直接使用,但它是构建更高层次并发工具的基础,例如java.util.concurrent.atomic包下的原子类如AtomicIntegerAtomicLong等。这些原子类通过JNI调用底层硬件提供的CAS指令,从而在Java层面上实现了无锁并发操作。

这里指的注意的是,在JDK1.9之前CAS机制被封装在sun.misc.Unsafe类中,在JDK1.9之后就使用了

jdk.internal.misc.Unsafe。这点由java.util.concurrent.atomic包下的原子类可以看出来。而sun.misc.Unsafe被许多第三方库所使用。

CAS实现原理

在Java中,虽然Java语言本身并未直接提供CAS这样的原子指令,但是Java可以通过JNI调用本地方法来利用硬件级别的原子指令实现CAS操作。在Java的标准库中,特别是jdk.internal.misc.Unsafe类提供了一系列compareAndSwapXXX方法,这些方法底层确实是通过C++编写的内联汇编来调用对应CPU架构的cmpxchg指令,从而实现原子性的比较和交换操作。

cmpxchg指令是多数现代CPU支持的原子指令,它能在多线程环境下确保一次比较和交换操作的原子性,有效解决了多线程环境下数据竞争的问题,避免了数据不一致的情况。例如,在更新一个共享变量时,如果期望值与当前值相匹配,则原子性地更新为新值,否则不进行更新操作,这样就能在无锁的情况下实现对共享资源的安全访问。

我们以java.util.concurrent.atomic包下的AtomicInteger为例,分析其compareAndSet方法。

public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L; //由这里可以看出来,依赖jdk.internal.misc.Unsafe实现的
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value"); private volatile int value; public final boolean compareAndSet(int expectedValue, int newValue) {
// 调用 jdk.internal.misc.Unsafe的compareAndSetInt方法
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
}

Unsafe中的compareAndSetInt使用了@HotSpotIntrinsicCandidate注解修饰,@HotSpotIntrinsicCandidate注解是Java HotSpot虚拟机(JVM)的一个特性注解,它表明标注的方法有可能会被HotSpot JVM识别为“内联候选”,当JVM发现有方法被标记为内联候选时,会尝试利用底层硬件提供的原子指令(比如cmpxchg指令)直接替换掉原本的Java方法调用,从而在运行时获得更好的性能。

public final class Unsafe {
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
}

compareAndSetInt这个方法我们可以从openjdkhotspot源码(位置:hotspot/src/share/vm/prims/unsafe.cpp)中可以找到:

{CC "compareAndSetObject",CC "(" OBJ "J" OBJ "" OBJ ")Z", FN_PTR(Unsafe_CompareAndSetObject)},

{CC "compareAndSetInt", CC "(" OBJ "J""I""I"")Z", FN_PTR(Unsafe_CompareAndSetInt)},

{CC "compareAndSetLong", CC "(" OBJ "J""J""J"")Z", FN_PTR(Unsafe_CompareAndSetLong)},

{CC "compareAndExchangeObject", CC "(" OBJ "J" OBJ "" OBJ ")" OBJ, FN_PTR(Unsafe_CompareAndExchangeObject)},

{CC "compareAndExchangeInt", CC "(" OBJ "J""I""I"")I", FN_PTR(Unsafe_CompareAndExchangeInt)},

{CC "compareAndExchangeLong", CC "(" OBJ "J""J""J"")J", FN_PTR(Unsafe_CompareAndExchangeLong)},

关于openjdk的源码,本文源码版本为1.9,如需要该版本源码或者其他版本下载方法,请关注本公众号【码农Academy】后,后台回复【openjdk】获取

hostspot中的Unsafe_CompareAndSetInt函数会统一调用Atomiccmpxchg函数:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {

oop p = JNIHandles::resolve(obj);

jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
// 统一调用Atomic的cmpxchg函数
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; } UNSAFE_END

Atomiccmpxchg函数源码(位置:hotspot/src/share/vm/runtime/atomic.hpp)如下:

/**
*这是按字节大小进行的`cmpxchg`操作的默认实现。它使用按整数大小进行的`cmpxchg`来模拟按字节大小进行的`cmpxchg`。不同的平台可以通过定义自己的内联定义以及定义`VM_HAS_SPECIALIZED_CMPXCHG_BYTE`来覆盖这个默认实现。这将导致使用特定于平台的实现而不是默认实现。
* exchange_value:要交换的新值。
* dest:指向目标字节的指针。
* compare_value:要比较的值。
* order:内存顺序。
*/
inline jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest,
jbyte compare_value, cmpxchg_memory_order order) {
STATIC_ASSERT(sizeof(jbyte) == 1);
volatile jint* dest_int =
static_cast<volatile jint*>(align_ptr_down(dest, sizeof(jint)));
size_t offset = pointer_delta(dest, dest_int, 1);
// 获取当前整数大小的值,并将其转换为字节数组。
jint cur = *dest_int;
jbyte* cur_as_bytes = reinterpret_cast<jbyte*>(&cur); // 设置当前整数中对应字节的值为compare_value。这确保了如果初始的整数值不是我们要找的值,那么第一次的cmpxchg操作会失败。
cur_as_bytes[offset] = compare_value; // 在循环中,不断尝试更新目标字节的值。
do {
// new_val
jint new_value = cur;
// 复制当前整数值,并设置其中对应字节的值为exchange_value。
reinterpret_cast<jbyte*>(&new_value)[offset] = exchange_value;
// 尝试使用新的整数值替换目标整数。
jint res = cmpxchg(new_value, dest_int, cur, order);
if (res == cur) break; // 如果返回值与原始整数值相同,说明操作成功。 // 更新当前整数值为cmpxchg操作的结果。
cur = res;
// 如果目标字节的值仍然是我们之前设置的值,那么继续循环并再次尝试。
} while (cur_as_bytes[offset] == compare_value);
// 返回更新后的字节值
return cur_as_bytes[offset];
}

而由cmpxchg函数中的do...while我们也可以看出,当多个线程同时尝试更新同一内存位置,且它们的期望值相同但只有一个线程能够成功更新时,其他线程的CAS操作会失败。对于失败的线程,常见的做法是采用自旋锁的形式,即循环重试直到成功为止。这种方式在低竞争或短时间窗口内的并发更新时,相比于传统的锁机制,它避免了线程的阻塞和唤醒带来的开销,所以它的性能会更优。

Java中的CAS实现与API

在Java中,CAS操作的实现主要依赖于两个关键组件:sun.misc.Unsafe类、jdk.internal.misc.Unsafe类以及java.util.concurrent.atomic包下的原子类。尽管Unsafe类提供了对底层硬件原子操作的直接访问,但由于其API是非公开且不稳定的,所以在常规开发中并不推荐直接使用。Java标准库提供了丰富的原子类,它们是基于Unsafe封装的安全、便捷的CAS操作实现。

java.util.concurrent.atomic

Java标准库中的atomic包为开发者提供了许多原子类,如AtomicIntegerAtomicLongAtomicReference等,它们均内置了CAS操作逻辑,使得我们可以在更高的抽象层级上进行无锁并发编程。



原子类中常见的CAS操作API包括:

  • compareAndSet(expectedValue, newValue):尝试将当前值与期望值进行比较,如果一致则将值更新为新值,返回是否更新成功的布尔值。
  • getAndAdd(delta):原子性地将当前值加上指定的delta值,并返回更新前的原始值。
  • getAndSet(newValue):原子性地将当前值设置为新值,并返回更新前的原始值。

这些方法都是基于CAS原理,能够在多线程环境下保证对变量的原子性修改,从而在不引入锁的情况下实现高效的并发控制。

CAS的优缺点与适用场景

CAS摒弃了传统的锁机制,避免了因获取和释放锁产生的上下文切换和线程阻塞,从而显著提升了系统的并发性能。并且由于CAS操作是基于硬件层面的原子性保证,所以它不会出现死锁问题,这对于复杂并发场景下的程序设计特别重要。另外,CAS策略下线程在无法成功更新变量时不需要挂起和唤醒,只需通过简单的循环重试即可。

但是,在高并发条件下,频繁的CAS操作可能导致大量的自旋重试,消耗大量的CPU资源。尤其是在竞争激烈的场景中,线程可能花费大量的时间在不断地尝试更新变量,而不是做有用的工作。这个由刚才cmpxchg函数可以看出。对于这个问题,我们可以参考synchronize中轻量级锁经过自旋,超过一定阈值后升级为重量级锁的原理,我们也可以给自旋设置一个次数,如果超过这个次数,就把线程挂起或者执行失败。(自适应自旋)

另外,Java中的原子类也提供了解决办法,比如LongAdder以及DoubleAdder等,LongAdder过分散竞争点来减少自旋锁的冲突。它并没有像AtomicLong那样维护一个单一的共享变量,而是维护了一个Base值和一组Cell(桶)结构。每个Cell本质上也是一个可以进行原子操作的计数器,多个线程可以分别在一个独立的Cell上进行累加,只有在必要时才将各个Cell的值汇总到Base中。这样一来,大部分时候线程间的修改不再是集中在同一个变量上,从而降低了竞争强度,提高了并发性能。

  1. ABA问题

    单纯的CAS无法识别一个值被多次修改后又恢复原值的情况,可能导致错误的判断。比如现在有三个线程:



    即线程1将str从A改成了B,然后线程3将str又从B改成了A,而此时对于线程2来说,他就觉得这个值还是A,所以就不会在更改了。

而对于这个问题,其实也很好解决,我们给这个数据加上一个时间戳或者版本号(乐观锁概念)。即每次不仅比较值,还会比较版本。比如上述示例,初始时str的值的版本是1,然后线程2操作后值变成B,而对应版本变成了2,然后线程3操作后值变成了A,版本变成了3,而对于线程2来说,虽然值还是A,但是版本号变了,所以线程2依然会执行替换的操作。

Java的原子类就提供了类似的实现,如AtomicStampedReferenceAtomicMarkableReference引入了附加的标记位或版本号,以便区分不同的修改序列。



总结

Java中的CAS原理及其在并发编程中的应用是一项非常重要的技术。CAS利用CPU硬件提供的原子指令,实现了在无锁环境下的高效并发控制,避免了传统锁机制带来的上下文切换和线程阻塞开销。Java通过JNI接口调用底层的CAS指令,封装在jdk.internal.misc类和java.util.concurrent.atomic包下的原子类中,为我们提供了简洁易用的API来实现无锁编程。

CAS在带来并发性能提升的同时,也可能引发循环开销过大、ABA问题等问题。针对这些问题,Java提供了如LongAdderAtomicStampedReferenceAtomicMarkableReference等工具类来解决ABA问题,同时也通过自适应自旋、适时放弃自旋转而进入阻塞等待等方式降低循环开销。

理解和熟练掌握CAS原理及其在Java中的应用,有助于我们在开发高性能并发程序时作出更明智的选择,既能提高系统并发性能,又能保证数据的正确性和一致性。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

美团一面:什么是CAS?有什么优缺点?我说我只用过AtomicInteger。。。。的更多相关文章

  1. CAS都不了解,你还怎么看J.U.C

    前言 说到CAS(CompareAndSwap),不得不先说一说悲观锁和乐观锁,因为CAS是乐观锁思想的一种实现. 悲观锁:总是很悲观的认为,每次拿数据都会有其他线程并发执行,所以每次都会进行加锁,用 ...

  2. java多线程-cas及atomic

    大纲: cas atomic 一.cas cas:compareAndSwap,一种乐观锁. cas思想:cas需要三个值,v是内存值,e是期望值,n是要修改的值.当内存中的值v等于预期值e(说明内存 ...

  3. 随意看看AtomicInteger类和CAS

    最近在读jdk源码,怎么说呢?感觉收获还行,比看框架源码舒服多了,一些以前就感觉很模糊的概念和一些类的用法也清楚了好多,举个很简单的例子,我在读Integer类的时候,发现了原来这个类自带缓存,看看如 ...

  4. Java性能 -- CAS乐观锁

    synchronized / Lock / CAS synchronized和Lock实现的同步锁机制,都属于悲观锁,而CAS属于乐观锁 悲观锁在高并发的场景下,激烈的锁竞争会造成线程阻塞,而大量阻塞 ...

  5. 深入浅出CAS

    后端开发中大家肯定遇到过实现一个线程安全的计数器这种需求,根据经验你应该知道我们要在多线程中实现 共享变量 的原子性和可见性问题,于是锁成为一个不可避免的话题,今天我们讨论的是与之对应的无锁 CAS. ...

  6. volatile、synchronized、ReentrantLock与CAS

    目录 一.JVM内存模型: 二.volatile关键字 1.volatile保证内存可见性. 2.能禁止指令重排序 3.不能保证原子性 三.synchronized关键字 1.内存可见性: 2.操作的 ...

  7. Java并发编程之CAS二源码追根溯源

    Java并发编程之CAS二源码追根溯源 在上一篇文章中,我们知道了什么是CAS以及CAS的执行流程,在本篇文章中,我们将跟着源码一步一步的查看CAS最底层实现原理. 本篇是<凯哥(凯哥Java: ...

  8. 一篇文章快速搞懂 Atomic(原子整数/CAS/ABA/原子引用/原子数组/LongAdder)

    前言 相信大部分开发人员,或多或少都看过或写过并发编程的代码.并发关键字除了Synchronized,还有另一大分支Atomic.如果大家没听过没用过先看基础篇,如果听过用过,请滑至底部看进阶篇,深入 ...

  9. JUC(二):CAS及ABA

    CAS是什么? 比较并交换. CAS示例 package com.chinda.java.audition; import java.util.concurrent.atomic.AtomicInte ...

  10. 第47天打卡学习(单例模式 深入了解CAS 原子引用 各种锁的理解)

    18彻底玩转 单例模式 饿汉式 DCL懒汉模式 探究! 饿汉式  package com.kuang.single; //饿汉式单例 //单例模式重要思想是构造器私有 public class Hun ...

随机推荐

  1. apache 服务器配置常用知识点合集

    前言 因为当年周围同学都在学php,最为简单的就是学php 好就业啊,写个一些php,最后放弃了,apache也看了两眼吧.下面是我使用有记录的,没有记录的我后面会补上. 正文 域名配置 1.取消 N ...

  2. ActiveMQ C#消息队列系列一(安装)

    前言 我前面写过ActiveMQ 如何在windows 上安装,但是呢,一般公司都是在Linux 上跑的,所以就来安装一下吧. 正文 1.进入官网下载linux 版本. 2.然后放置到Linux上. ...

  3. TiDB、OceanBase、PolarDB-X、CockroachDB二级索引写入性能测评

    简介: 二级索引是关系型数据库相较于NoSQL数据库的一个关键差异.二级索引必须是强一致的,因此索引的写入需要与主键的写入放在一个事务当中,事务的性能是二级索引性能的基础.本次测试将重点关注不同分布式 ...

  4. 一种关于低代码平台(LCDP)建设实践与设计思路

    简介: 作者在负责菜鸟商业中心CRM系统开发过程中发现有一个痛点:业务线很多,每个业务线对同一个页面都有个性化布局和不同的字段需求,而他所在的团队就3个人,那么在资源有限的情况下该如何支撑呢?本文就降 ...

  5. 揭秘 RocketMQ 新特性以及在金融场景下的实践

    2019 年末, RocketMQ 正式发布了 4.6.0 版本,增加了" Request-Reply "的同步调用的新特性." Request-Reply " ...

  6. 高效使用Java构建工具|Maven篇|云效工程师指北

    ​简介:高效使用Java构建工具|Maven篇.众所周知,当前最主流的Java构建工具为Maven/Gradle/Bazel,针对每一个工具,我将分别从日常工作中常见的场景问题切入,例如依赖管理.构建 ...

  7. [FAQ] GitHub 开启二次验证之后,如何通过 https clone 项目 ?

    在 Github Personal Access Tokens 页面,点击生成一个新的 tokon. 此时使用这个 token 作为用户的密码来 clone 项目.

  8. WPF 通过 InputManager 模拟调度触摸事件

    在 WPF 中,框架可以分为两个部分,一个是渲染,另一个是交互.交互的入口是在 InputManager 里面,而实际的交互实现需要通过渲染布局和交互的路由事件才能完成.在输入管理提供了调度事件的方法 ...

  9. 2024-04-27:用go语言,在一个下标从 1 开始的 8 x 8 棋盘上,有三个棋子,分别是白色车、白色象和黑色皇后。 给定这三个棋子的位置,请计算出要捕获黑色皇后所需的最少移动次数。 需要注意

    2024-04-27:用go语言,在一个下标从 1 开始的 8 x 8 棋盘上,有三个棋子,分别是白色车.白色象和黑色皇后. 给定这三个棋子的位置,请计算出要捕获黑色皇后所需的最少移动次数. 需要注意 ...

  10. 应用zabbix的实时导出(real-time export)功能

    说明 zabbix作为监控软件,有时也会需要获取历史数据作进一步的分析,通常可以采用3种办法: 通过zabbix API定期获取(通过web) 通过后端数据库定期读取(通过db) 应用实时导出功能配合 ...