JUC-AQS与ReentrantLock
CAS
1、什么是CAS
CAS(Compare And Swap,比较与交换),非堵塞同步实现原理,是cpu硬件层面的一种指令。通过硬件实现比较与交换的原子性。
CAS指令包括三个参数:内存值V(要修改的变量)、预期值E、新值N。执行时先读取当前值,如果当前值等于预期值,则将内存修改为新值,否则不做修改。无论修改与否都会返回旧的内存值。整个过程是原子的,由 CPU 指令保证。
2、Java 中的 CAS 实现
sun.misc.Unsafe 类提供了底层的 CAS 方法,如
publicfinalnativebooleancompareAndSwapInt(Objectobj,longoffset,intexpect,intupdate);publicfinalnativebooleancompareAndSwapObject(Objectobj,longoffset,Objectexpect,Objectupdate);参数解释:
- obj:对象实例,
- offset:内存偏移量、
- expect:字段期望值、
- update:字段新值
//获取unsafe对象FieldtheUnsafe=Unsafe.class.getDeclaredField("theUnsafe");theUnsafe.setAccessible(true);Unsafeunsafe=(Unsafe)theUnsafe.get(null);//获取字段内存偏移量longx=unsafe.objectFieldOffset(Entity.class.getDeclaredField("x"));Entityentity=newEntity();booleanb;b=unsafe.compareAndSwapObject(entity,x,null,"abc");System.out.println("1b->"+b);// trueb=unsafe.compareAndSwapObject(entity,x,null,"abc");System.out.println("2b->"+b);// falsenative 方法直接调用 CPU 的 CAS 指令,Java 并发包(java.util.concurrent.atomic)中的原子类(如 AtomicInteger)正是基于 Unsafe 的 CAS 实现。
3、CAS 的优点
- 无锁:不会引起线程阻塞与上下文切换,在高并发,低竞争场景下优于synchronized。可以看作是乐观锁的一种实现方式。
- 轻量级:不需要操作系统调度,减小开销
- 乐观并发:冲突少时,失败重试,适合读多写少场景
4、CAS 的缺点
1:自旋开销
- CAS失败时通常会在循环中重试(自旋),如果长时间不成功会给cpu带来大量开销。所以CAS适用于短时间竞争
2:只能保证单个变量的原子操作
- 无法保证多个变量同时操作
3:ABA问题
- 当一个内存值变量从a改成b,又从b改回a,则CAS认为没有发生任何变化,但中间可能发生其他操作;
例如:Thread 1、2、3需要修改变量 a过程
1、thread 1 获取内存值a=1,此时线程被挂起,
2、thread 2 获取内存值a=1,修改为 a=2,
3、thread 3 获取内存值a=2,修改为 a=1,
4、thread 1 恢复后执行 CAS,发现当前值仍然是 1(与期望值相同),认为a没有被其他线程修改过,继续执行后续操作。
5、ABA问题的解决方案
通过版本号/标记位是否改变来检测内存值是否发生变化。
版本号:基于数据版本实现数据同步的机制,每次修改一次数据,版本就会进行累加(如数据库乐观锁)。
Java 提供了 AtomicStampedReference 和 AtomicMarkableReference 原子引用类来解决 ABA 问题。
AtomicStampedReference :内部维护 [reference, stamp] 对,每次修改时同时比较引用和版本号(stamp),修改后的版本号往上递增,通过版本号检测 “值被改过又改回” 的中间变化。reference 并不是直接的内存地址数值,而是 对象引用(即指向堆中对象的“指针”)
AtomicStampedReference 比较的是引用是否指向同一个对象与 stamp(版本戳) 是否相等。
AtomicMarkableReference:类似,但版本号只有 true/false,适用于只需标记是否修改过的场景。
6、CAS应用场景
- 原子类:AtomicInteger、AtomicLong、AtomicReference 等。
- AQS(AbstractQueuedSynchronizer):用于同步状态 state 的更新和 CLH 队列的入队操作。
- 并发容器:如 ConcurrentHashMap 在 Java 8 中使用 CAS 进行初始化、节点替换等。
- 自旋锁:简单的自旋锁可通过 CAS 实现。
7、Atomic原子操作类
实现方式:
- 原子性:通过 Unsafe 类直接调用 CPU 原子指令,实现“比较并交换”的原子性
- volatile:保证CAS 操作后其他线程能立即看到最新值,保证变量的内存可见性
- 无锁:多个线程同时操作时,失败的线程会自旋重试,而不是立即挂起
Atomic分类:
- 基本类型:对 int、long、boolean 进行原子更新,如 AtomicInteger、AtomicLong、AtomicBoolean
- 数组类型:对数组元素进行原子更新,如:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 引用类型:对对象引用进行原子更新,如:AtomicReference、AtomicStampedReference、AtomicMarkableReference,后两者用于解决 ABA 问题。
- 字段更新器: 基于反射,对普通 volatile 字段进行原子更新(减少对象包装开销),如:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
- 累加器(JDK 8+): 用于高并发统计场景,性能优于 AtomicLong(内部分段累加,最后汇总),如 LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator
volatile
volatile 是 Java 的一种轻量级同步机制,用于修饰变量。它保证可见性和有序性,但不保证原子性。
使用场景:
1:状态标志(开关):状态的变化对其他线程立即可见,且无需加锁。
2:双重检查锁(DCL)中的单例:volatile 防止 instance = new Singleton() 内部指令重排(分配内存、初始化、赋值)导致其他线程读到未完全初始化的对象。
3:读多写少、无需复合操作的配置参数。
总结:
volatile 是一个轻量、高效的同步工具,适用于一个线程写、多个线程读的简单状态标志,或需要禁止重排序的场合。对于复合操作,仍应使用锁或原子类。正确使用 volatile 可以避免过度同步,提升性能。
AbstractQueuedSynchronizer (AQS)
1、什么是AQS
AbstractQueuedSynchronizer 是 java.util.concurrent 包的基石,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等同步器都基于它实现。AQS 提供了一个框架,用于实现依赖 状态(state) 和 FIFO 等待队列 的阻塞锁和同步器。
2、AQS核心结构
1. 同步状态 state-> private volatile int state;
state表示资源的可用状态
- ReentrantLock:0 表示未锁定,>0 表示锁定(可重入计数)。
- Semaphore:表示剩余许可数。
- CountDownLatch:表示需要倒计数的数量。
2. CLH 队列(FIFO 同步队列)
作用:维护获取锁失败时入队的线程
CLH 队列一种基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,
- 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时阻塞当前线程。
- 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
- 通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)
Node 节点:
AQS 内部维护一个 双向链表,节点 Node 包含:
- thread:等待的线程
- prev、next:前驱和后继指针
- waitStatus:等待状态(CANCELLED=1, SIGNAL=-1, CONDITION=-2, PROPAGATE=-3)
- nextWaiter:用于条件队列的链接
队列结构:
head<->Node1<->Node2<->...<->tail- head 是当前持有锁的线程节点,tail 指向队尾。
- 入队操作通过 CAS 设置 tail 实现(无锁并发)。
- 出队时,head 指向下一个线程节点。
waitStatus状态:
- 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
- CANCELLED,值为1,表示当前的线程被取消;
- SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
- CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
- PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;
3. 条件队列
- 使用单向列表保存,
- 使用nextWaiter连接
- 调用await方法阻塞线程,当前线程存在于同步队列的头结点(从同步队列转化到条件队列)。
AQS 内部实现了 Condition 接口。每个 Condition 对象拥有一个单向队列(链表 Node),用于存放调用 await() 而阻塞的线程。当其他线程调用 signal() 时,会将条件队列中的节点转移到同步队列,等待获取锁。
3、ReentrantLock
ReentrantLock 是基于 AQS 实现的可重入独占锁。支持公平/非公平选择、可响应中断、超时获取、多条件变量。
公平锁与非公平锁
ReentrantLock 默认非公平锁,但构造时可指定 fair = true 创建公平锁。
ReentrantLock 内部维护一个 Sync 抽象类,继承自 AbstractQueuedSynchronizer。实现非公平锁NonfairSync(默认)
与公平锁FairSync
AQS中state字段
ReentrantLock 中使用AQS中state字段表示重入次数。
- state = 0:锁空闲。
- state > 0:锁被某个线程持有,数值表示重入次数(同一线程多次 lock 会递增)
公平锁与非公平锁的区别
非公平锁:新线程在 state=0 时立即尝试 CAS 抢锁,不管同步队列中是否有等待线程。
公平锁:新线程在 state=0 时会先检查队列中是否有前驱等待线程,若队列中已有等待线程,则入队排队,不参与抢占。
lock() 非公平锁加锁流程:
1、快速尝试加锁
当前线程调用lock立即通过CAS修改 AQS 的 state 属性值 ,尝试将state 从 0 改为 1。
成功:当前线程获取锁,记录锁的持有者为当前线程(exclusiveOwnerThread = currentThread),重入次数为1;
失败:执行AQS的acquire方法,进入常规获取锁流程
2、常规获取锁过程
再次尝试获取锁,使用tryAcquire(1)方法,获取当前 state,
(1)若 state == 0(锁空闲),再次 CAS 尝试获取锁,成功则设置独占线程为当前线程,返回 true。
(2)若 state > 0 且当前线程就是锁的持有者:执行可重入 state + 1,返回 true
其他情况返回false,抢锁失败。然加入同步队列(addWaiter())
3、加入同步队列(addWaiter)
当 tryAcquire 返回 false 时,AQS 会为当前线程创建一个 Node 节点(模式为独占),并通过 CAS 将其插入到 CLH 队列的尾部。
如果队列未初始化,会先初始化一个空的头节点(enq 方法自旋)。
4、自旋与挂起(acquireQueued)
节点入队后检查自己的前驱节点是否为头节点(head),如果是,尝试获取锁资源,
若获取成功:将当前节点设为新的头节点,原头节点出队(help GC),线程返回,加锁成功。
获取失败或者不是头节点:将前驱节点的 waitStatus 设为-1(表示后继需要被唤醒)。
然后挂起当前线程(LockSupport.park)。
总结:
快速 CAS 尝试 → 可重入检查 → 失败后入队 → 自旋阻塞 → 被唤醒后竞争锁。
非公平锁允许在 state=0 时进行插队,而公平锁严格遵循队列顺序。
整个流程完全基于 AQS 的 state + CLH 队列 + CAS + LockSupport 实现,保证了高并发下的正确性与性能。
公平锁加锁流程(FairSync)
公平锁加锁不会进行快速尝试加锁,使用常规获取锁过程,并且在state == 0时,公平锁会先判断同步队列中是否存在等待时间更长的线程(即队列非空且当前线程不是头节点的后继)。如果存在,加锁失败,当前线程必须排队,不能抢占锁。
只有队列为空或当前线程本身就是队首等待线程时,才允许 CAS 获取锁。
锁释放过程:
- 计算新的 state 值:c = state - 1(因为 releases=1)。
- 检查当前线程是否为锁的持有者,若不是则抛出 异常。
- 如果 c == 0,说明锁已经完全释放(所有重入锁已退出),将 exclusiveOwnerThread 设为 null,表示锁不再被任何线程持有。 通过 setState© 更新 AQS 的 state 字段(注意:这里不需要 CAS,因为锁由当前线程独占,没有并发写)
- 锁已完全空闲,release 唤醒后继线程
获取当前队列的头节点 head,如果 head 不为空且 waitStatus == -1,从队列中找到头节点之后第一个未被取消的节点,调用 LockSupport.unpark(thread) 唤醒该节点对应的线程。
被唤醒线程的后续动作
被唤醒的线程原先因为 park() 而阻塞,一旦被 unpark,继续执行自旋循环:
- 检查自己的前驱是否为头节点。如果是,则尝试获取锁。
成功:将自己设为新头节点,加锁成功。
失败:(极端情况,如又被非公平锁抢先),可能再次自旋或阻塞。
释放锁总结:
- 减少 AQS 的 state 值(重入计数减 1)。
- 如果 state 变为 0,则清空独占线程记录,并通知 AQS 锁已空闲。
- AQS 检查同步队列头节点,如果头节点 waitStatus == SIGNAL,则唤醒其后继节点。
- 唤醒操作通过 LockSupport.unpark 实现,被唤醒线程将重新竞争锁
Condition
Condition 是 JUC 中提供的线程等待/通知机制,必须与 ReentrantLock 配合使用。其核心实现是 AQS 的内部类 ConditionObject,它利用了 AQS 的同步队列和条件队列,实现了高效的等待/通知
Condition整体结构
每个 ConditionObject 维护一个条件队列(单向链表),用来存放调用了 await() 而阻塞的线程节点。同时,AQS 本身还有一个同步队列(CLH 双向队列),用来存放竞争锁而阻塞的线程。
- 一个 Lock 可以创建多个 Condition 实例(如 notFull、notEmpty)。
- 每个 Condition 拥有自己独立的条件队列。
- 节点在不同队列间转移:await 时从同步队列进入条件队列;signal 时从条件队列回到同步队列。
node节点结构
- 在同步队列中,nextWaiter 可以用来表示节点是独占模式还是共享模式(Node.EXCLUSIVE / Node.SHARED)。
- 在条件队列中,nextWaiter 指向下一个条件等待节点
- 当节点在条件队列中时,其 waitStatus 被设置为 CONDITION = -2。
核心方法:
void await():
- 必须在持有锁时调用,调用该方发释放当前线程锁持有锁,并创建nodo节点加入等待队列。
boolean await(long time, TimeUnit unit)
- 必须在持有锁时调用,调用该方发释放当前线程锁持有锁,并创建nodo节点加入等待队列,
等待time时间超时后移入同步队列,获取锁后执行返回false,表示超时唤醒。
等待time时间超时内移入同步队列,获取锁后执行返回true,表示是满足条件唤醒。
返回值只与超时是否发生有关,与最终是否获取锁无关。
await 的返回值是为了让调用者知道条件是否在期望的时间内变成真
如果超时后条件才变成真,那对于需要实时响应的业务来说,已经太晚了(例如等待某个资源可用,但超时后决定放弃操作)。即使最终获得了锁,业务逻辑也应该根据返回值判断是否要继续等待条件,而不是简单依赖于锁的获取。
void signal():
- 必须在持有锁时调用,将在此条件队列上的一个线程,移入同步队列,并不立即释放锁,只有执行到reentrantLock.unlock后才会释放锁。
void signalAll()
- 与 signal 类似,但它会遍历整个条件队列,将每个节点都转移到同步队列,并不立即释放锁,只有执行到reentrantLock.unlock后才会释放锁。
转移后的节点如何获取锁
公平锁:转移后的节点获取锁的顺序等于它被转移到同步队列的顺序(也等于它在条件队列中等待的顺序,因为条件队列也是 FIFO)。不会有新来的线程插在它前面。
非公平锁:转移后的节点可能被后续新来的线程插队(入队前直接CAS抢锁),获取锁的时间不确定,但最终总能获取到(除非锁被永久占用)。这就是非公平锁的“吞吐量优先”特性。还有可能被其他正在lock的线程直接CAS抢锁。
因此,在非公平锁下,从条件队列转移出来的线程很可能需要等待更长时间(甚至比后来才请求锁的新线程更晚获得锁)
