深入理解Java高并发编程(5) - 无锁并发/乐观锁

1. CAS

CAS(Compare and Swap) 是一种无锁操作,包括以下三个操作数

  • 内存地址/变量地址:要修改的变量地址
  • 期望的原值
  • 期望的新值

CAS的逻辑是通过将要赋值时候,比较要修改的变量值是否还等于原值,等于才把原值改为新值,否则则不进行任何操作,它的底层是依赖CPU的硬件指令(如 x86 架构的 CMPXCHG 指令),由硬件保证原子性,效率很高。

CAS需要配合Volatile使用,因为CAS需要借助volatile拿到变量的当前最新值来实现比较和交换。CAS仅仅是保证了赋值操作的原子性。

CAS适合线程数少的时候使用,尽量避免线程上下文切换

CAS操作的优点

  • 无锁,开销小
  • 多核CPU下CAS的线程上下文切换少(时间片耗尽发生),CAS通过线程不断运行不断尝试,减少线程阻塞,减小线程上下文切换的开销。
  • 充分利用cpu资源

2. 原子类

JUC包下的原子类实现了CAS操作,底层是通过持有一个unsafe的成员变量,通过操纵unsafe的native方法实现CAS(正如我们之前所说,CAS操作底层依赖CPU的硬件指令

原子类内部会维护一个volatile的value值,使得原子类保证了原子性,可见性,有序性。
image-20260311183301113

2.1 原子整数

  • AtomicBoolean

  • AtomicInteger:对四字节整数封装,使其操作是原子的

    • compareAndSet:只做一次CAS操作,成功返回true。

    • getAndUpdate:接受一个IntUnaryOperator的函数式接口,在里面写lambda表达式执行具体的计算逻辑,底层还是调用compareAndSet搭配while循环。

image-20260311184702308

  • AtomicLong:对八字节整数封装,使其操作是原子的

2.2 原子引用类型

保证引用类型变量的线程安全。

  • AtomicReference
  • AtomicStampedReference
  • AtomicMarkableReference

ABA问题:AtomicReference能保证修改时值是没有发生变化的,但却不能保证对象有没有被修改过(也就是从A修改成B又修改成A,检测的时候还是两个A,依然可以CAS),这时候就可以用AtomicStampedReferenceAtomicMarkableReference

  • AtomicStampedReference
    • 构造函数可以传入对象和一个int类型作为时间戳(版本号),每次完成一次修改都去修改这个时间戳,表示发生了一次修改
    • AtomicStampedReference能得知对象发生了多少次修改
  • AtomicMarkableReference
    • AtomicMarkableReference关注对象是否发生了修改
    • 构造函数传入对象和boolean类型表示是否被修改过

2.3 原子数组

当存在多线程操作数组中元素的场景,可以用到原子数组类来保证线程安全。

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

2.4 字段更新器

利用字段更新器,可以针对对象某个field进行原子操作,只能配合volatile修饰的字段使用,否则会出现异常。

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

2.5 原子累加器

jdk8后新增了几个累加类,且性能相对原子整数类的累加更好,更适合做累加。

因为累加器通过设置多个累加单元,在多线程的场景下,不同的线程操作不同的累加单元,最后再汇总,而原子整数被多线程累加后,可能存在线程竞争阻塞,这就导致了其性能不如原子累加器。

  1. 原子累加器:

    • LongAdder:只能做相加,底层还是调用LongAccumulator方法,会检查是否有二元运算函数,因为没有所以是默认累加。

    • LongAccumulator:支持自己定义二元的LongBinaryOperator并进行运算

  2. 原理:

    • LongAdder类的关键域:
  //累加单元数组,懒惰初始化
  transient volatile Cell[] cells;
  //基础值,但没有竞争,就使用这个域cas累加
  transient volatile long base;
  //在cells创建或扩容时置为1,表示加锁
  transient volatile int cellsBusy
  1. CAS锁:
  • CAS虽然是无锁的,也可以用有锁的思想,像这里去修改cellsBusy就是CAS(1表示加锁,0表示未加锁),这里也是为了保护共享变量cellsBusy多线程时的线程安全。
  1. 伪共享问题
  • 伪共享问题指的是,在cpu中分为多级缓存,因为cpu直接去内存中找耗时较高,cpu会先把数据读到缓存中,而缓存是以缓存行为单位,一般是64byte,不同cpu核心可能会把数据读到同一个缓存的缓存行中,当某个核心修改了这个缓存行的数据,这个cpu核心和其他所有cpu核心都要重新去读当前修改数据所在的缓存行,这就是伪共享问题。

  • Cell的源码中,@sun.misc.Contended注解是为了避免伪共享问题的注解,一个cell一般是24字节,这就导致了多个cell可能出现在多个cpu核心的同一个缓存的缓存行中。假设cell[0] 和cell[1]放在一个缓存行中,当核心1去修改cell[0],核心2的缓存行就会失效,就得重新从内存中读数据。@sun.misc.Contended通过在每个cell的前后加了128byte的空隙,使得不同cell放在了不同的缓存行,这样就能避免伪共享的问题。

image-20260311221207514

LongAdder add原理:

  • 当调用add方法时候,先判断cells是否为null
    • 如果为null 就使用base进行CAS累加
      • 如果CAS累加成功,就return掉
      • 如果失败了就调用longAccumulate方法进行cells的创建
    • 当cells不为null时候
      • 去判断当前线程是否创建了cell,如果创建了,就在当前线程对应的cell上累加,累加成功就return,失败了则调用longAccumulate方法。
      • 如果当前线程没创建cell,也是调用longAccumulate方法

image-20260311222733097

image-20260311223829244

3. Unsafe类

Unsafe对象提供了非常底层,操作内存,线程的方法,之前的CAS,park/unpark 中都是调用了Unsafe中的方法。Unsafe对象不能直接调用,只能通过反射获得。

posted @ 2026-04-08 16:07  不会coding的喵酱  阅读(10)  评论(0)    收藏  举报