文章目录
1. AQS和CAS
多线程中经常听到AQS和CAS,他们究竟是什么呢?
1.1 AQS:
AbstractQueuedSynchronizer(AQS)抽象队列同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch等。在实现AQS时,一般通过定义内部类Sync继承AQS,将同步器所有调用都映射到Sync对应的方法来处理。
AQS定义两种资源共享方式:
-
①:独占:只有一个线程能执行,如
ReentrantLockstatic final Node EXCLUSIVE = null; -
②:共享:多个线程可以同时执行,如
Semaphore/CountDownLatchstatic final Node SHARED = new Node();
AQS定义两种队列:
-
①:同步等待队列 :
AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人 发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; }
同步等待队列的进入时机:
-
当前线程获取锁失败后,同步器会将线程构建成一个节点,并将其加入同步队列中。
-
通过
signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)
-
②:条件等待队列:
Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),只有当该条件具备时,这些等待线程才会被唤醒,从而重新争夺锁Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; }
条件等待队列的进入时机:
- 调用
await方法阻塞线程;
注意:一个线程只能存在于两个队列中的一个
AQS四大核心原理
- 队列元素自旋获取锁:阻塞的线程通过自旋不断尝试加锁,加锁成功则跳出循环
- LockSupport:通过
LockSupport.park()和LockSupport.unpark() - CAS:无锁算法,功能类似于Synchronized
- 队列:保存每个阻塞线程的引用
=========================================================
1.2 CAS
compare and swap(CAS),比较与交换。作用:不管并发有多高,都能保证操作的原子性,保证锁的互斥性。CAS是保证并发安全性的一条CPU底层原子指令cmpxchg(x86框架下),它的功能是判断某个值是否为预期值,如果是的话,就改为新值,在CAS过程中不会被中断。
但CPU底层原子指令cmpxchg仅仅能保证原子性,如果java中的CAS只有这项功能肯定是不行的!因为并发安全需要原子性、一致性、可见性都得到保证,java中的compareAndSwapXxx方法其实在jvm底层又增加了Lock前缀去保证其他并发安全特性的!所以在java中可以使用CAS达到Sychronized、Lock同样的功能
然而在Java发展初期,并不能直接利用硬件提供的并发来提升系统的性能的,随着java的发展,CAS理论成为实现java并发的一种常用手段。
CAS包含三个参数:
- ①:偏移量:要更新的变量(V)在内存中的偏移量
- ②:预期值 (A)
- ③:新值(B)
什么是偏移量:从堆内存中对象头的Mark Word(起始位置)开始数,直到存储该成员变量V的起始值,这个起始值被称为当前对象的成员变量V的偏移量,也就是内存中记录V的位置,用于cpu寻址,进而进行cas中的比较!!
如果内存位置的值与预期值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值
对于某个对象中的变量V,如果想要通过CAS无锁且安全的方式修改这个变量V的值,具体的逻辑如下:
- 通过cpu寻址找到该变量
V在对象中的偏移量,拿到V的原始值。 - 拿
V的原始值和预期值(A)作比较 - 如果当前值和预期值一样,则修改
V的值为新值(B) - 如果当前值和预期值不一样,则不能修改
V的原始值,可以选择自旋,重新获取V的值,直到V的值等于预期值(A)才可以对V进行修改!
如以上逻辑,通过CAS操作,保证整个操作过程是原子的,任意时刻只有一个线程执行成功!
cas的实现:
主要是使用的Unsafe类的这三个native方法,再底层就是调用的汇编的原子指令cmpxchg(),依赖于硬件完成!
/**
* Object var1:对象
* long var2:对象中某变量的偏移量
* int var4:期望值
* int var5:想要修改的值
*/
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
1.1 CAS存在的bug:ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值 原来是A,变成了B,然后又变成了A,那么此时使用CAS进行检查时会发现它的值没有发生变化,依旧是A,最终结果没有变。但是实际上,A却经历了A-B-A的过程,这就是CAS存在的ABA问题!
ABA问题示例
//要修改的变量
private static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);
public static void main(String[] args) {
//线程一 修改后又恢复原样
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
},"t1").start();
//线程二:还可以修改,无法解决ABA问题
new Thread(() -> {
try {
//睡一秒,让线程一先修改
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t修改后的值:" + atomicReference.get());
},"t2").start();
}
- 初始值为100,线程t1将100改成101,然后又将101改回100
- 线程t2先睡眠1秒,等待t1操作完成,然后t2线程将值改成2019
- 可以看到,线程2修改成功
- 输出结果:true 修改后的值:2019
ABA问题的危害
一般场景下,使用CAS处理一些数值的简单计算,ABA并不会出现什么问题,但是当涉及到引用的时候就会出问题。
比如:使用CAS导致栈结构变化
- 现有一个用单向链表实现的堆栈,栈顶为
A,这时线程T1已经知道A.next为B

- 然后希望用
CAS将栈顶替换为B,使栈中元素为B、B:head.compareAndSet(A,B) - 在
T1执行上面这条指令之前,由于T1还没有进入CAS,存在线程争抢!此时线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

- 此时轮到线程
T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

- 最后堆栈中只有
B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。
ABA解决方案:
使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号+1,那么A->B->A 就会变成1A->2B->3A,在进行CAS比较时,即使值相同,也不应该修改成功,还要比对版本号!从JDK1.5 开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。AtomicStampedReference除了定义值 ,还可以定义版本号
// AtomicStampedReference定义值为100 ,版本号为1
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);
public static void main(String[] args) {
//线程一
new Thread(() -> {
System.out.println("t1拿到的初始版本号:" + atomicStampedReference.getStamp());
//睡眠1秒,是为了让t2线程也拿到同样的初始版本号
TimeUnit.SECONDS.sleep(1);
//干坏事,修改原始值
atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
},"t1").start();
//线程二
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println("t2拿到的初始版本号:" + stamp);
//睡眠3秒,是为了让t1线程完成ABA操作
TimeUnit.SECONDS.sleep(3);
System.out.println("最新版本号:" + atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(100, 2019,stamp,atomicStampedReference.getStamp() + 1) + "\t当前 值:" + atomicStampedReference.getReference());
},"t2").start();
}
- 初始值100,初始版本号1
- 线程t1和t2拿到一样的初始版本号1
- 线程t1完成ABA操作,版本号递增到3

本文深入解析了AQS(AbstractQueuedSynchronizer)和CAS(Compare-and-Swap)的原理,探讨了它们在Java多线程环境中的应用,包括ReentrantLock与synchronized的区别,ReentrantLock的内部结构及工作流程,以及如何预防死锁。
最低0.47元/天 解锁文章
4626

被折叠的 条评论
为什么被折叠?



