Jtoss Jtoss
首页
  • 数据结构与算法

    • 数据结构与算法 - 概述
    • 数据结构与算法 - 复杂度分析
    • 数据结构 - 线性表
    • 算法 - 常见排序算法
  • 代码规范

    • 代码简洁之道
    • 阿里巴巴开发手册
    • 谷歌Java编程风格指南
  • 设计模式

    • 编写高质量代码概述
    • 面向对象
    • 设计原则
    • 设计模式-创建型
    • 设计模式-结构型
    • 设计模式-行为型(上)
    • 设计模式-行为型(下)
    • 浅析框架源码中的设计模式
    • 业务框架实战案例
  • MySQL 基础

    • MySQL - 数据库设计规范
    • MySQL - 必知必会
  • MySQL 进阶

    • MySQL - 基础架构
    • MySQL - InnoDB存储引擎
    • MySQL - InnoDB缓冲池
    • MySQL - 事务与锁
    • MySQL - 索引
    • MySQL - 查询执行计划
    • MySQL - 性能优化
  • Redis 系列

    • Redis入门 - 基础相关
    • Redis进阶 - 数据结构
    • Redis进阶 - 持久化RDB和AOF
    • Redis进阶 - 事件机制
    • Redis进阶 - 事务
    • Redis进阶 - 高可用高可扩展
    • Redis进阶 - 缓存问题
    • Redis进阶 - 性能调优
  • Java 基础

    • Java 基础 - 知识点
    • Java 基础 - 面向对象
    • Java 基础 - Q/A
  • Java 进阶 - 集合框架

    • Java 集合框架详解
  • Java 进阶 - 多线程与并发

    • Java 并发 - 理论基础
    • Java 并发 - 线程基础
    • Java 并发 - 各种锁
    • Java 并发 - 关键字 volatile
    • Java 并发 - 关键字 synchronized
    • JUC - CAS与原子操作
    • JUC - 锁核心类AQS
    • JUC - 锁接口和类简介
    • JUC - 并发容器简介
    • JUC - 通信工具类
    • JUC - Fork-Join框架
    • JUC - 线程池
  • Java 进阶 - JVM

    • JVM - 概述
    • JVM - 类加载机制
    • JVM - 内存结构
    • JVM - 垃圾回收机制
    • JVM - 性能调优
  • Maven系列

    • Maven基础知识
    • Maven项目构建
    • Maven多模块配置
  • Spring 框架

    • Spring 框架 - 框架介绍
    • Spring 框架 - IOC详解
    • Spring 框架 - AOP详解
    • Spring 框架 - SpringMVC详解
  • Spring Boot 系列

    • Spring Boot - 开发入门
    • Spring Boot - 接口相关
  • Spring Cloud 系列
  • Mybatis 系列

    • Mybatis - 总体框架设计
    • Mybatis - 初始化基本过程
    • Mybatis - sqlSession执行过程
    • Mybatis - 插件机制
    • Mybatis - 事务管理机制
    • Mybatis - 缓存机制
  • 业务常见问题

    • Java 业务开发常见错误(一)
    • Java 业务开发常见错误(二)
    • Java 业务开发常见错误(三)
    • Java 业务开发常见错误(四)
    • Java 业务开发常见错误(五)
    • Java 业务开发常见错误(六)
  • IDEA系列

    • IDEA 2021开发环境配置
    • IDEA 快捷键
  • Git系列

    • git status中文乱码
  • 其他

    • Typora+Picgo 自动上传图片
    • hsdis 和 jitwatch
  • 实用技巧
  • 收藏
  • 摄影
  • 学习
  • 标签
  • 归档

Jason Huang

后端程序猿
首页
  • 数据结构与算法

    • 数据结构与算法 - 概述
    • 数据结构与算法 - 复杂度分析
    • 数据结构 - 线性表
    • 算法 - 常见排序算法
  • 代码规范

    • 代码简洁之道
    • 阿里巴巴开发手册
    • 谷歌Java编程风格指南
  • 设计模式

    • 编写高质量代码概述
    • 面向对象
    • 设计原则
    • 设计模式-创建型
    • 设计模式-结构型
    • 设计模式-行为型(上)
    • 设计模式-行为型(下)
    • 浅析框架源码中的设计模式
    • 业务框架实战案例
  • MySQL 基础

    • MySQL - 数据库设计规范
    • MySQL - 必知必会
  • MySQL 进阶

    • MySQL - 基础架构
    • MySQL - InnoDB存储引擎
    • MySQL - InnoDB缓冲池
    • MySQL - 事务与锁
    • MySQL - 索引
    • MySQL - 查询执行计划
    • MySQL - 性能优化
  • Redis 系列

    • Redis入门 - 基础相关
    • Redis进阶 - 数据结构
    • Redis进阶 - 持久化RDB和AOF
    • Redis进阶 - 事件机制
    • Redis进阶 - 事务
    • Redis进阶 - 高可用高可扩展
    • Redis进阶 - 缓存问题
    • Redis进阶 - 性能调优
  • Java 基础

    • Java 基础 - 知识点
    • Java 基础 - 面向对象
    • Java 基础 - Q/A
  • Java 进阶 - 集合框架

    • Java 集合框架详解
  • Java 进阶 - 多线程与并发

    • Java 并发 - 理论基础
    • Java 并发 - 线程基础
    • Java 并发 - 各种锁
    • Java 并发 - 关键字 volatile
    • Java 并发 - 关键字 synchronized
    • JUC - CAS与原子操作
    • JUC - 锁核心类AQS
    • JUC - 锁接口和类简介
    • JUC - 并发容器简介
    • JUC - 通信工具类
    • JUC - Fork-Join框架
    • JUC - 线程池
  • Java 进阶 - JVM

    • JVM - 概述
    • JVM - 类加载机制
    • JVM - 内存结构
    • JVM - 垃圾回收机制
    • JVM - 性能调优
  • Maven系列

    • Maven基础知识
    • Maven项目构建
    • Maven多模块配置
  • Spring 框架

    • Spring 框架 - 框架介绍
    • Spring 框架 - IOC详解
    • Spring 框架 - AOP详解
    • Spring 框架 - SpringMVC详解
  • Spring Boot 系列

    • Spring Boot - 开发入门
    • Spring Boot - 接口相关
  • Spring Cloud 系列
  • Mybatis 系列

    • Mybatis - 总体框架设计
    • Mybatis - 初始化基本过程
    • Mybatis - sqlSession执行过程
    • Mybatis - 插件机制
    • Mybatis - 事务管理机制
    • Mybatis - 缓存机制
  • 业务常见问题

    • Java 业务开发常见错误(一)
    • Java 业务开发常见错误(二)
    • Java 业务开发常见错误(三)
    • Java 业务开发常见错误(四)
    • Java 业务开发常见错误(五)
    • Java 业务开发常见错误(六)
  • IDEA系列

    • IDEA 2021开发环境配置
    • IDEA 快捷键
  • Git系列

    • git status中文乱码
  • 其他

    • Typora+Picgo 自动上传图片
    • hsdis 和 jitwatch
  • 实用技巧
  • 收藏
  • 摄影
  • 学习
  • 标签
  • 归档
  • Java 基础

  • Java 进阶 - 集合框架

  • Java 进阶 - 多线程与并发

    • Java 并发 - 概述
    • Java 并发 - 理论基础
    • Java 并发 - 线程基础
    • Java 并发 - 各种锁
    • Java 并发 - JVM 锁优化
    • Java 并发 - 关键字 volatile
    • Java 并发 - 关键字 synchronized
    • Java 并发 - syschronized 应用及死锁问题
    • Java 并发 - 关键字 final
    • JUC - CAS与原子操作
      • 1. 乐观锁与悲观锁的概念
      • 2. CAS 的概念
      • 3. CAS 使用示例
      • 3. Java 实现 CAS 的原理 - Unsafe 类
      • 4. CAS 实现原子操作的三大问题
        • 4.1 ABA 问题
        • 4.2 循环时间长开销大
        • 4.3 只保证一个共享变量的原子操作
      • 5. 原子操作 - AtomicInteger类简析
      • 6. 其他原子类举例
      • 7. 如何解决 CAS 的 ABA 问题
      • 8. 参考
    • JUC - 锁核心类AQS
    • JUC - 锁接口和类简介
    • JUC - 并发容器简介
    • JUC - 阻塞队列
    • JUC - 通信工具类
    • JUC - Fork/Join框架
    • JUC - Stream并行计算原理
    • JUC - 线程池
  • Java 进阶 - JVM

  • Java 进阶 - 版本特性

  • Java
  • Java 进阶 - 多线程与并发
Jason
目录

JUC - CAS与原子操作

# JUC - CAS与原子操作

  • 线程安全的实现方法有哪些?
  • 什么是 CAS?
  • CAS使用示例,结合 AtomicInteger 给出示例?
  • CAS 会有哪些问题?
  • 针对这这些问题,Java 提供了哪几个解决的?
  • AtomicInteger 底层实现? CAS + volatile
  • 请阐述你对 Unsafe 类的理解?
  • 说说你对 Java 原子类的理解? 包含 13 个,4 组分类,说说作用和使用场景。
  • AtomicStampedReference 是什么?
  • AtomicStampedReferenc e是怎么解决 ABA 的? 内部使用 Pair 来存储元素值及其版本号
  • java中还有哪些类可以解决 ABA 的问题? AtomicMarkableReference

# 1. 乐观锁与悲观锁的概念

锁可以从不同的角度分类。其中,乐观锁和悲观锁是一种分类方式。

悲观锁:

悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

乐观锁:

乐观锁又称为“无锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为 CAS 的技术来保证线程执行的安全性。

由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁天生免疫死锁。

乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

# 2. CAS 的概念

CAS的全称是:比较并交换(Compare And Swap)。

在CAS中,有这样三个值:

  • V:要更新的变量(var)
  • E:预期值(expected)
  • N:新值(new)

比较并交换的过程如下:

判断 V 是否等于 E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。

所以这里的预期值E本质上指的是“旧值”。

我们以一个简单的例子来解释这个过程:

  1. 如果有一个多个线程共享的变量i原本等于 5,我现在在线程 A 中,想把它设置为新的值6;
  2. 我们使用 CAS 来做这个事情;
  3. 首先我们用i去与 5 对比,发现它等于5,说明没有被其它线程改过,那我就把它设置为新的值 6,此次 CAS 成功,i的值被设置成了 6;
  4. 如果不等于 5,说明i被其它线程改过了(比如现在i的值为 2),那么我就什么也不做,此次 CAS 失败,i的值仍然为 2。

在这个例子中,i就是 V,5 就是 E,6 就是N。

那有没有可能我在判断了i为 5 之后,正准备更新它的新值的时候,被其它线程更改了i的值呢?

不会的。因为 CAS 是一种原子操作,它是一种系统原语,是一条 CPU 的原子指令,从 CPU 层面保证它的原子性

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

JDK 中大量使用了 CAS 来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。

相信 SQL 大家都熟悉,类似sql中的条件更新一样:update set id=3 from table where id=2。因为单条 sql 执行具有原子性,如果有多个线程同时执行此 sql 语句,只有一条能更新成功。

# 3. CAS 使用示例

如果不使用 CAS,在高并发下,多线程同时修改一个变量的值我们需要 synchronized 加锁(可能有人说可以用Lock 加锁,Lock 底层的 AQS 也是基于 CAS 进行获取锁的)。

public class Test {
    private int i=0;
    public synchronized int add(){
        return i++;
    }
}
1
2
3
4
5
6

java 中为我们提供了AtomicInteger 原子类(底层基于 CAS 进行更新数据的),不需要加锁就在多线程并发场景下实现数据的一致性。

public class Test {
    private  AtomicInteger i = new AtomicInteger(0);
    public int add(){
        return i.addAndGet(1);
    }
}
1
2
3
4
5
6

# 3. Java 实现 CAS 的原理 - Unsafe 类

前面提到,CAS 是一种原子操作。那么 Java 是怎样来使用 CAS 的呢?我们知道,在 Java 中,如果一个方法是native 的,那 Java 就不负责具体实现它,而是交给底层的 JVM 使用 c 或者 c++ 去实现。

在 Java 中,有一个Unsafe类,它在sun.misc包中。从源码中发现,内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试)。

public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var4));

    return var5;
}
1
2
3
4
5
6
7
8

且原子操作其实只支持下面三个方法:

boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
boolean compareAndSwapInt(Object o, long offset,int expected,int x);
boolean compareAndSwapLong(Object o, long offset,long expected,long x);
1
2
3

当然,他们都是public native的。

Unsafe 中对 CAS 的实现是 C++ 写的,它的具体实现和操作系统、CPU 都有关系。它是一个本地方法,实现位于unsafe.cpp 中。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
1
2
3
4
5
6

可以看到它通过 Atomic::cmpxchg 来实现比较和替换操作(这个指令在 CPU 级完成 CAS 操作的)。其中参数 x 是即将更新的值,参数 e 是原内存的值。

如果是 Linux的x86,Atomic::cmpxchg方法的实现如下:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}
1
2
3
4
5
6
7
8

而windows的x86的实现如下:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    int mp = os::isMP(); //判断是否是多处理器
    _asm {
        mov edx, dest
        mov ecx, exchange_value
        mov eax, compare_value
        LOCK_IF_MP(mp)
        cmpxchg dword ptr [edx], ecx
    }
}

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

如果是多处理器,为 cmpxchg 指令添加 lock 前缀。反之,就省略 lock 前缀(单处理器会不需要 lock 前缀提供的内存屏障效果)。这里的 lock 前缀就是使用了处理器的总线锁(最新的处理器都使用缓存锁代替总线锁来提高性能)。

当然,Unsafe 类里面还有其它方法用于不同的用途。比如支持线程挂起和恢复的park和unpark, LockSupport类底层就是调用了这两个方法。还有支持反射操作的allocateInstance()方法。

# 4. CAS 实现原子操作的三大问题

# 4.1 ABA 问题

所谓 ABA 问题,就是一个值原来是 A,变成了 B,又变回了 A。这个时候使用 CAS 是检查不出变化的,但实际上却被更新了两次。

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。从 JDK 1.5 开始,JDK 的 atomic 包里提供了一个类AtomicStampedReference类来解决 ABA 问题。

这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用 CAS 设置为新的值和标志。

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}
1
2
3
4
5
6
7
8
9
10
11
12

# 4.2 循环时间长开销大

CAS 多与自旋结合。如果自旋 CAS 长时间不成功,会占用大量的 CPU 资源。

解决思路是让 JVM 支持处理器提供的 pause 指令。pause指令有两个作用:

  • 第一,它可以延迟流水线执行命令 (de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;
  • 第二,它可以避免在退出循环的时候因内存顺序冲突 (Memory Order Violation) 而引起 CPU 流水线被清空 (CPU Pipeline Flush),从而提高 CPU 的执行效率。

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

这个问题你可能已经知道怎么解决了。有两种解决方案:

  1. 使用JDK 1.5开始就提供的AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行CAS 操作;
  2. 使用锁。锁内的临界区代码可以保证只有当前线程能操作。

# 5. 原子操作 - AtomicInteger类简析

上面介绍了 Unsafe 类的几个支持 CAS 的方法。那 Java 具体是如何使用这几个方法来实现原子操作的呢?

JDK 提供了一些用于原子操作的类,在java.util.concurrent.atomic包下面。在JDK 11 中,有如下 17 个类:

java-concurrent-atomic-class

从名字就可以看得出来这些类大概的用途:

  • 原子更新基本类型
  • 原子更新数组
  • 原子更新引用
  • 原子更新字段(属性)

以 AtomicInteger 为例,常用 API:

public final int get():获取当前的值
public final int getAndSet(int newValue):获取当前的值,并设置新的值
public final int getAndIncrement():获取当前的值,并自增
public final int getAndDecrement():获取当前的值,并自减
public final int getAndAdd(int delta):获取当前的值,并加上预期的值
void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
1
2
3
4
5
6

相比 Integer 的优势,多线程中让变量自增:

private volatile int count = 0;
// 若要线程安全执行执行 count++,需要加锁
public synchronized void increment() {
    count++;
}
public int getCount() {
    return count;
}
1
2
3
4
5
6
7
8

使用 AtomicInteger 后:

private AtomicInteger count = new AtomicInteger();
public void increment() {
    count.incrementAndGet();
}
// 使用 AtomicInteger 后,不需要加锁,也可以实现线程安全
public int getCount() {
    return count.get();
}
1
2
3
4
5
6
7
8

源码分析

这里我们以AtomicInteger类的getAndAdd(int delta)方法为例,来看看 Java 是如何实现原子操作的。

先看看这个方法的源码:

public final int getAndAdd(int delta) {
    return U.getAndAddInt(this, VALUE, delta);
}
1
2
3

这里的 U 其实就是一个Unsafe对象:

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
1

所以其实AtomicInteger类的getAndAdd(int delta)方法是调用Unsafe类的方法来实现的:

@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}
1
2
3
4
5
6
7
8

注:这个方法是在JDK 1.8 才新增的。在 JDK1.8 之前,AtomicInteger源码实现有所不同,是基于 for 死循环的,有兴趣的读者可以自行了解一下。

我们来一步步解析这段源码。首先,对象o是this,也就是一个AtomicInteger对象。然后offset是一个常量VALUE。这个常量是在AtomicInteger类中声明的:

private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
1

同样是调用的Unsafe的方法。从方法名字上来看,是得到了一个对象字段偏移量。

用于获取某个字段相对 Java 对象的“起始地址”的偏移量。

一个 java 对象可以看成是一段内存,各个字段都得按照一定的顺序放在这段内存里,同时考虑到对齐要求,可能这些字段不是连续放置的,

用这个方法能准确地告诉你某个字段相对于对象的起始内存地址的字节偏移量,因为是相对偏移量,所以它其实跟某个具体对象又没什么太大关系,跟 class 的定义和虚拟机的内存模型的实现细节更相关。

继续看源码。前面我们讲到,CAS 是“无锁”的基础,它允许更新失败。所以经常会与 while 循环搭配,在失败后不断去重试。

这里声明了一个 v,也就是要返回的值。从getAndAddInt来看,它返回的应该是原来的值,而新的值的v + delta。

这里使用的是 do-while 循环。这种循环不多见,它的目的是保证循环体内的语句至少会被执行一遍。这样才能保证return 的值v是我们期望的值。

循环体的条件是一个 CAS 方法:

public final boolean weakCompareAndSetInt(Object o, long offset,
                                          int expected,
                                          int x) {
    return compareAndSetInt(o, offset, expected, x);
}

public final native boolean compareAndSetInt(Object o, long offset,
                                             int expected,
                                             int x);
1
2
3
4
5
6
7
8
9

可以看到,最终其实是调用的我们之前说到了 CAS native方法。那为什么要经过一层weakCompareAndSetInt呢?从 JDK 源码上看不出来什么。在 JDK 8 及之前的版本,这两个方法是一样的。

而在 JDK 9 开始,这两个方法上面增加了 @HotSpotIntrinsicCandidate 注解。这个注解允许 HotSpot VM自己来写汇编或 IR 编译器来实现该方法以提供性能。也就是说虽然外面看到的在 JDK9 中weakCompareAndSet 和 compareAndSet 底层依旧是调用了一样的代码,但是不排除 HotSpot VM 会手动来实现 weakCompareAndSet 真正含义的功能的可能性。

同时可以看到 AtomicInteger 底层用的是 volatile 的变量和 CAS 来进行更改数据的。

简单来说,weakCompareAndSet操作仅保留了volatile自身变量的特性,而除去了 happens-before 规则带来的内存语义。也就是说,weakCompareAndSet**无法保证处理操作目标的 volatile 变量外的其他变量的执行顺序( 编译器和处理器为了优化程序性能而对指令序列进行重新排序 ),同时也无法保证这些变量的可见性。**这在一定程度上可以提高性能。

再回到循环条件上来,可以看到它是在不断尝试去用 CAS 更新。如果更新失败,就继续重试。那为什么要把获取“旧值” v 的操作放到循环体内呢?其实这也很好理解。前面我们说了,CAS 如果旧值 V不 等于预期值 E,它就会更新失败。说明旧的值发生了变化。那我们当然需要返回的是被其他线程改变之后的旧值了,因此放在了 do 循环体内。

# 6. 其他原子类举例

  1. 原子更新数组

    通过原子的方式更新数组里的某个元素,Atomic包提供了以下的3个类:

    • AtomicIntegerArray: 原子更新整型数组里的元素。
    • AtomicLongArray: 原子更新长整型数组里的元素。
    • AtomicReferenceArray: 原子更新引用类型数组里的元素。

    这三个类的最常用的方法是如下两个方法:

    • get(int index):获取索引为index的元素值。
    • compareAndSet(int i,E expect,E update): 如果当前值等于预期值,则以原子方式将数组位置 i 的元素设置为update 值。

    举个 AtomicIntegerArray 例子:

    import java.util.concurrent.atomic.AtomicIntegerArray;
    
    public class Demo5 {
        public static void main(String[] args) throws InterruptedException {
            AtomicIntegerArray array = new AtomicIntegerArray(new int[] { 0, 0 });
            System.out.println(array);
            System.out.println(array.getAndAdd(1, 2));
            System.out.println(array);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    输出结果:

    [0, 0]
    0
    [0, 2]
    
    1
    2
    3
  2. 原子更新引用类型

    Atomic包提供了以下三个类:

    • AtomicReference: 原子更新引用类型。
    • AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。
    • AtomicMarkableReferce: 原子更新带有标记位的引用类型。

    这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象 set 进 Atomic 类,然后调用compareAndSet 等一些方法去进行原子操作,原理都是基于 Unsafe 实现,但 AtomicReferenceFieldUpdater略有不同,更新的字段必须用 volatile 修饰。

    举个 AtomicReference 例子:

    import java.util.concurrent.atomic.AtomicReference;
    public class AtomicReferenceTest {
        public static void main(String[] args){
            // 创建两个Person对象,它们的id分别是101和102。
            Person p1 = new Person(101);
            Person p2 = new Person(102);
            // 新建AtomicReference对象,初始化它的值为p1对象
            AtomicReference ar = new AtomicReference(p1);
            // 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。
            ar.compareAndSet(p1, p2);
    
            Person p3 = (Person)ar.get();
            System.out.println("p3 is "+p3);
            System.out.println("p3.equals(p1)="+p3.equals(p1));
        }
    }
    
    class Person {
        volatile long id;
        public Person(long id) {
            this.id = id;
        }
        public String toString() {
            return "id:"+id;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    结果输出:

    p3 is id:102
    p3.equals(p1)=false
    
    1
    2

    结果说明:

    • 新建 AtomicReference 对象 ar 时,将它初始化为 p1。
    • 紧接着,通过 CAS 函数对它进行设置。如果 ar 的值为 p1 的话,则将其设置为 p2。
    • 最后,获取 ar 对应的对象,并打印结果。p3.equals(p1) 的结果为 false,这是因为 Person 并没有覆盖equals() 方法,而是采用继承自 Object.java 的 equals() 方法;而 Object.java 中的 equals() 实际上是调用"=="去比较两个对象,即比较两个对象的地址是否相等。
  3. 原子更新字段类

    Atomic包提供了四个类进行原子字段更新:

    • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
    • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
    • AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。

    这四个类的使用方式都差不多,是基于反射的原子更新字段的值。要想原子地更新字段类需要两步:

    • 第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法 newUpdater() 创建一个更新器,并且需要设置想要更新的类和属性。
    • 第二步,更新类的字段必须使用 public volatile 修饰。

    举个例子:

    public class TestAtomicIntegerFieldUpdater {
    
        public static void main(String[] args){
            TestAtomicIntegerFieldUpdater tIA = new TestAtomicIntegerFieldUpdater();
            tIA.doIt();
        }
    
        public AtomicIntegerFieldUpdater<DataDemo> updater(String name){
            return AtomicIntegerFieldUpdater.newUpdater(DataDemo.class,name);
    
        }
    
        public void doIt(){
            DataDemo data = new DataDemo();
            System.out.println("publicVar = "+updater("publicVar").getAndAdd(data, 2));
            /*
                * 由于在DataDemo类中属性value2/value3,在TestAtomicIntegerFieldUpdater中不能访问
                * */
            //System.out.println("protectedVar = "+updater("protectedVar").getAndAdd(data,2));
            //System.out.println("privateVar = "+updater("privateVar").getAndAdd(data,2));
    
            //System.out.println("staticVar = "+updater("staticVar").getAndIncrement(data));//报java.lang.IllegalArgumentException
            /*
                * 下面报异常:must be integer
                * */
            //System.out.println("integerVar = "+updater("integerVar").getAndIncrement(data));
            //System.out.println("longVar = "+updater("longVar").getAndIncrement(data));
        }
    
    }
    
    class DataDemo{
        public volatile int publicVar=3;
        protected volatile int protectedVar=4;
        private volatile  int privateVar=5;
    
        public volatile static int staticVar = 10;
        //public  final int finalVar = 11;
    
        public volatile Integer integerVar = 19;
        public volatile Long longVar = 18L;
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43

    再说下对于AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束,约束如下:

    • 字段必须是volatile类型的,在线程之间共享变量时保证立即可见.eg:volatile int value = 3
    • 字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。
    • 只能是实例变量,不能是类变量,也就是说不能加static关键字。
    • 只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。
    • 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。

# 7. 如何解决 CAS 的 ABA 问题

Java 通过 AtomicStampedReference 类解决 CAS 的 ABA 问题。AtomicStampedReference 主要维护包含一个对象引用以及一个可以自动更新的整数 "stamp" 的 pair 对象来解决 ABA 问题。

public class AtomicStampedReference<V> {
    private static class Pair<T> {
        final T reference;  //维护对象引用
        final int stamp;  //用于标志版本
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
    private volatile Pair<V> pair;
    ....
    
    /**
      * expectedReference :更新之前的原始值
      * newReference : 将要更新的新值
      * expectedStamp : 期待更新的标志版本
      * newStamp : 将要更新的标志版本
      */
    public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
        // 获取当前的(元素值,版本号)对
        Pair<V> current = pair;
        return
            // 引用没变
            expectedReference == current.reference &&
            // 版本号没变
            expectedStamp == current.stamp &&
            // 新引用等于旧引用
            ((newReference == current.reference &&
            // 新版本号等于旧版本号
            newStamp == current.stamp) ||
            // 构造新的Pair对象并CAS更新
            casPair(current, Pair.of(newReference, newStamp)));
    }

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        // 调用Unsafe的compareAndSwapObject()方法CAS更新pair的引用为新引用
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
    ....
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
  • 如果元素值和版本号都没有变化,并且和新的也相同,返回 true;
  • 如果元素值和版本号都没有变化,并且和新的不完全相同,就构造一个新的 Pair 对象并执行 CAS 更新 pair。

可以看到,java 中的实现跟我们上面讲的 ABA 的解决方法是一致的。

  • 首先,使用版本号控制;
  • 其次,不重复使用节点 (Pair) 的引用,每次都新建一个新的 Pair 来作为 CAS 比较的对象,而不是复用旧的;
  • 最后,外部传入元素值及版本号,而不是节点 (Pair) 的引用。

使用举例

public class AtomicTester {

    private static AtomicStampedReference<Integer> atomicStampedRef =
            new AtomicStampedReference<>(1, 0);

    public static void main(String[] args){
        first().start();
        second().start();
    }

    private static Thread first() {
        return new Thread(() -> {
            System.out.println("操作线程" + Thread.currentThread() +",初始值 a = " + atomicStampedRef.getReference());
            int stamp = atomicStampedRef.getStamp(); //获取当前标识别
            try {
                Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
            System.out.println("操作线程" + Thread.currentThread() +",CAS操作结果: " + isCASSuccess);
        },"主操作线程");
    }

    private static Thread second() {
        return new Thread(() -> {
            Thread.yield(); // 确保thread-first 优先执行
            atomicStampedRef.compareAndSet(1,2,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
            System.out.println("操作线程" + Thread.currentThread() +",【increment】 ,值 = "+ atomicStampedRef.getReference());
            atomicStampedRef.compareAndSet(2,1,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
            System.out.println("操作线程" + Thread.currentThread() +",【decrement】 ,值 = "+ atomicStampedRef.getReference());
        },"干扰线程");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

输出结果:

操作线程Thread[主操作线程,5,main],初始值 a = 1
操作线程Thread[干扰线程,5,main],【increment】 ,值 = 2
操作线程Thread[干扰线程,5,main],【decrement】 ,值 = 1
操作线程Thread[主操作线程,5,main],CAS操作结果: false
1
2
3
4

Java 中的 AtomicMarkableReference 也可以解决 ABA 问题,但它不是维护一个版本号,而是维护一个boolean类型的标记,标记值有修改。

# 8. 参考

  • https://pdai.tech/md/java/thread/java-thread-x-juc-AtomicInteger.htm
  • http://concurrent.redspider.group/article/02/10.html
#多线程与并发#CAS
上次更新: 2024-08-19
Java 并发 - 关键字 final
JUC - 锁核心类AQS

← Java 并发 - 关键字 final JUC - 锁核心类AQS→

最近更新
01
开始
01-09
02
AI工具分享
01-09
03
AI 导读
01-07
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Jason Huang | 闽ICP备2025088096号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式