CAS 的全称是 Compare And Swap,是 CPU 并发原语
- CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法,调用 UnSafe 类中的 CAS 方法,JVM 会实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,实现了原子操作
- CAS 是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且原语的执行必须是连续的,执行过程中不允许被中断,所以 CAS 是一条 CPU 的原子指令,不会造成数据不一致的问题,是线程安全的
底层原理:CAS 的底层是
lock cmpxchg
指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性
- 程序在单核处理器上运行时,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果
- 程序在多核处理器上运行时,会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时,CPU 会执行总线锁定或缓存锁定,将修改的变量写入到主存,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性
作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存的值一致为止
无锁(乐观锁)
★CAS
CAS 的全称是 Compare And Swap,是 CPU 并发原语
- CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法,调用 UnSafe 类中的 CAS 方法,JVM 会实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,实现了原子操作
- CAS 是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且原语的执行必须是连续的,执行过程中不允许被中断,所以 CAS 是一条 CPU 的原子指令,不会造成数据不一致的问题,是线程安全的
底层原理:CAS 的底层是
lock cmpxchg
指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性- 程序在单核处理器上运行时,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果
- 程序在多核处理器上运行时,会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时,CPU 会执行总线锁定或缓存锁定,将修改的变量写入到主存,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性
作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存的值一致为止
CAS 与 volatile:
- 获取共享变量时,为了保证该变量的可见性,需要使用 volatile 关键字修饰。它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,而必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
- CAS 必须借助 volatile 才能读取到共享变量的最新值来实现 “比较并交换” 的效果。
CAS 特点:
- CAS 体现的是无锁并发、无阻塞并发,线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用)
- CAS 是基于乐观锁的思想
CAS 缺点:
- 执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,使用 CAS 线程数不要超过 CPU 的核心数,采用分段 CAS 和自动迁移机制
- 只能保证一个共享变量的原子操作
- 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作
- 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候只能用锁来保证原子性
CAS 与 synchronized 的比较:
- synchronized 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,性能较差
- CAS 是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值,CAS 这种机制也称之为乐观锁,综合性能较好
Atomic类
原子整数(针对基本数据类型)
原子整数类:
AtomicInteger
、AtomicBoolean
、AtomicLong
以
AtomicInteger
为例。构造方法:
public AtomicInteger()
:初始化一个默认值为 0 的原子型 Integerpublic AtomicInteger(int initialValue)
:初始化一个指定值的原子型 Integer
常用 API:
方法 作用 public final int get() 获取 AtomicInteger 的值 public final int getAndIncrement() 以原子方式将当前值加 1,返回的是自增前的值 public final int incrementAndGet() 以原子方式将当前值加 1,返回的是自增后的值 public final int getAndSet(int value) 以原子方式设置为 newValue 的值,返回旧值 public final int addAndGet(int data) 以原子方式将输入的数值与实例中的值相加并返回 实例:AtomicInteger 里的 value
AtomicInteger 原理:自旋锁 + CAS 算法
- CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改的值 B)
- 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
- 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋
原子引用(针对引用变量)
原子引用:对 Object 进行原子操作,提供一种读和写都是原子性的对象引用变量。
和前面的原子整数类似,原子整数是对基本数据类型的修改进行原子操作,原子引用是对引用变量的修改进行原子操作。
- 原子引用类:
AtomicReference
、AtomicStampedReference
、AtomicMarkableReference
AtomicReference
只看值能不能对的上,只要对的上就能改AtomicStampedReference
除了看值对不对的上,还要看这期间是不是改过好几次了,也就是说虽然值对的上,但有可能值已经改过好几次了,只是最后又改回到原来的值,改动的次数用版本号stamp
来记录AtomicMarkableReference
不关心改动了几次,只关心是否更改过,是否更改过用mark
来记录
- 原子引用类:
以
AtomicReference
为例。- 构造方法:
AtomicReference<T> atomicReference = new AtomicReference<T>()
- 常用 API:
public final boolean compareAndSet(V expectedValue, V newValue)
:CAS 操作public final void set(V newValue)
:将值设置为 newValuepublic final V get()
:返回当前值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class AtomicReferenceDemo {
public static void main(String[] args) {
Student s1 = new Student(33, "AA");
// 创建原子引用包装类
AtomicReference<Student> atomicReference = new AtomicReference<>();
// 设置主内存共享变量为s1
atomicReference.set(s1);
// 比较并交换,如果现在主物理内存的值为 AA,那么交换成 BB
while (true) {
Student s2 = new Student(44, "BB");
if (atomicReference.compareAndSet(s1, s2)) {
break;
}
}
System.out.println(atomicReference.get());
}
}
class Student {
private int id;
private String name;
//...
}- 构造方法:
原子数组(针对数组数据)
- 原子数组类:
AtomicIntegerArray
、AtomicLongArray
、AtomicReferenceArray
原子更新器(针对对象字段)
- 原子更新器类:
AtomicReferenceFieldUpdater
、AtomicIntegerFieldUpdater
、AtomicLongFieldUpdater
- 利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
IllegalArgumentException: Must be volatile type
- 利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
原子累加器(针对基本数据类型的累加操作)
原子累加器类:
LongAdder
、DoubleAdder
、LongAccumulator
、DoubleAccumulator
LongAdder 和 LongAccumulator 区别:
相同点:
LongAdder 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的
LongAdder 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr
不同点:
调用 casBase 时,LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算,LongAdder 使用 casBase(b = base, b + x)
LongAccumulator 类功能更加强大,构造方法参数中
- accumulatorFunction 是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值,LongAdder 内置累加规则
- identity 则是 LongAccumulator 累加器的初始值,LongAccumulator 可以为累加器提供非 0 的初始值,而 LongAdder 只能提供默认的 0
LongAdder
LongAdder 是 Java8 提供的类,跟 AtomicLong 有相同的效果,但对 CAS 机制进行了优化,尝试使用分段 CAS 机制以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能。
- CAS 底层实现是在一个循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈修改成功率很高,否则失败率很高,失败后这些重复的原子性操作会耗费性能(导致大量线程空循环,自旋转)
- LongAdder 将 AtomicLong 的单点的更新压力分担到各个节点,空间换时间,在低并发的时候直接更新,可以保障和 AtomicLong 的性能基本一致,而在高并发的时候通过分散减少竞争,提高了性能
分段 CAS 机制:
- 在发生竞争时,创建 Cell 数组用于将不同线程的操作离散(通过 hash 等算法映射)到不同的节点上
- 设置多个累加单元(会根据需要扩容,最大为 CPU 核数),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后将结果汇总
- 在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能
自动分段迁移机制:某个 Cell 的 value 执行 CAS 失败,就会自动寻找另一个 Cell 分段内的 value 值进行 CAS 操作
unsafe
- Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地(Native)方法来访问
- Unsafe 类存在 sun.misc 包,其中所有方法都是 native 修饰的,都是直接调用操作系统底层资源执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似 C 的指针