美团一面:什么是CAS?有什么优缺点?我说我只用过AtomicInteger。。。。

cas,atomicinteger · 浏览次数 : 0

小编点评

**Java 中 CAS 的原理及其应用** **引言** Java 中的 CAS (Compare-and-Swap) 是一个高效的并发编程技术,它利用 CPU 的硬件原子指令来实现无锁环境下的高效并发控制。 **CAS 的关键组件** * `Unsafe` 类:提供对底层硬件原子操作的直接访问,但其 API 是非公开且不稳定的,所以通常不推荐直接使用。 * `AtomicInteger`、`AtomicLong` 等类:基于 `Unsafe`封装的安全、便捷的 CAS 操作实现,提供了许多原子类,如 `AtomicInteger`、`AtomicLong`、`AtomicReference`等,均内置了 CAS 操作逻辑,使我们在更高的抽象层级上进行无锁并发编程。 **CAS 的优点** * 避免了传统锁机制带来的上下文切换和线程阻塞开销。 * 提高了系统的并发性能。 **CAS 的缺点** * 在高并发条件下,频繁的 CAS 操作可能导致大量的自旋重试,消耗大量的 CPU 资源。 **Java 中的原子类** Java 提供了一些原子类,如 `AtomicInteger`、`AtomicLong` 等,用于实现 CAS 操作。这些类提供类似的实现,例如 `AtomicStampedReference` 和 `AtomicMarkableReference`,可以解决 ABA (Atomicity, Barrier and Sequential)问题。 **应用** CAS 是一种非常重要的技术,可以用于实现各种并发编程模式,如: * **无锁并发编程:**可以有效地提高系统的并发性能。 * **ABA问题的解决:**可以解决在 CAS 操作中出现的问题,如线程间竞争或版本冲突。 * **自适应自旋:**可以减少循环开销。 **结论** Java 中的 CAS 是一种强大的并发技术,可以极大地提高系统并发性能。然而,在高并发条件下,频繁的 CAS 操作可能导致性能瓶颈,因此需要谨慎使用。

正文

引言

传统的并发控制手段,如使用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操作逻辑,使得我们可以在更高的抽象层级上进行无锁并发编程。
image.png
原子类中常见的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中。这样一来,大部分时候线程间的修改不再是集中在同一个变量上,从而降低了竞争强度,提高了并发性能。

image.png

  1. ABA问题
    单纯的CAS无法识别一个值被多次修改后又恢复原值的情况,可能导致错误的判断。比如现在有三个线程:
    image.png
    即线程1将str从A改成了B,然后线程3将str又从B改成了A,而此时对于线程2来说,他就觉得这个值还是A,所以就不会在更改了。

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

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

image.png
image.png

总结

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。。。。相似的内容:

美团一面:什么是CAS?有什么优缺点?我说我只用过AtomicInteger。。。。

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

美团一面问我i++跟++i的区别是什么

美团一面问我i++跟++i的区别是什么 面试官:“i++跟++i的区别是什么?” 我:“i++是先使用然后再执行+1的操作,++i是先执行+1的操作然后再去使用i” 面试官:“那你看看下面这段代码,运行结果是什么?” public static void main(String[] args) {

美团VS饿了么,到底谁更胜一筹?

最近啊,收到一个粉丝的投稿,我发现他在美团和饿了么都去面试过。 这俩企业大家应该都经常用吧,咱点外卖的时候,我有时候就琢磨,到底他俩谁更厉害点。 今天咱们就瞅瞅,在面试这块儿谁更难一些。 (目前都只有一面的情况,要是想要后续的,私聊我发给你哈) 美团 一面 自我介绍 项目做完了吗?背景是什么?项目初

DevOps infra | 互联网、软件公司基础设施建设(基建)哪家强?

国内公司普遍不注重基础设施建设,这也是可以理解的。吃饭都吃不饱,就别提什么荤素搭配,两菜一汤了。但也不能全说是这样,还是有很多公司投入大量的人力物力去做好公司的基建,比如很多阿里和美团的小伙伴对公司的基建还是很认可的。 为什么工程师都很在意公司的基建 有人说再好的磨盘也只是提升了驴拉磨的效率,便宜了

美团面试:说说Netty的零拷贝技术?

零拷贝技术(Zero-Copy)是一个大家耳熟能详的技术名词了,它主要用于提升 IO(Input & Output)的传输性能。 那么问题来了,为什么零拷贝技术能提升 IO 性能? 1.零拷贝技术和性能 在传统的 IO 操作中,当我们需要读取并传输数据时,我们需要在用户态(用户空间)和内核态(内核空

美团面试拷打:ConcurrentHashMap 为何不能插入 null?HashMap 为何可以?

周末的时候,有一位小伙伴提了一些关于 `ConcurrentHashMap` 的问题,都是他最近面试遇到的。原提问如下: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9fa94f132705416a8e07e33907544113~tp

美团面试:如何实现线程任务编排?

线程任务编排指的是对多个线程任务按照一定的逻辑顺序或条件进行组织和安排,以实现协同工作、顺序执行或并行执行的一种机制。 1.线程任务编排 VS 线程通讯 有同学可能会想:那线程的任务编排是不是问的就是线程间通讯啊? 线程间通讯我知道了,它的实现方式总共有以下几种方式: Object 类下的 wait

美团携手HarmonyOS SDK,开启便捷生活新篇章

华为开发者大会(HDC 2024)于6月21日在东莞松山湖拉开序幕,通过一系列精彩纷呈的主题演讲、峰会、专题论坛和互动体验,为开发者们带来了一场知识与技术的盛宴。6月23日,《HarmonyOS开放能力,使能应用原生易用体验》分论坛成功举办,美团作为鸿蒙原生应用开发中的优秀案例,受邀出席了此次活动。

抢先看!美团、京东、360等大厂面试题解析,技术面试必备。

技术面试必备!美团、京东、360等大厂面试题详解,让你轻松应对各大公司面试挑战! 往期硬核面经 哦耶!冲进腾讯了! 牛逼!上岸腾讯互娱和腾讯TEG! 腾讯的面试,强度拉满! 前几篇文章分享了上岸腾讯的最新面经。 不少粉丝股东留言说别只发腾讯的啦,其他大厂的也安排一些吧,比如美团、360、京东的。 必

美团二面:SpringBoot读取配置优先级顺序是什么?

理解并合理运用Spring Boot配置加载的优先级,对于保障应用的安全性、可维护性以及降低部署复杂度至关重要。特别是在大规模微服务架构中,合理的配置管理和迁移对于整体系统的稳定性有着不可忽视的作用。