线程池(三):深入理解CAS、volatile与AQS
线程池(三):深入理解CAS、volatile与AQS
一、CAS(Compare and Swap,比较并交换)
2.4.1 概述及基本工作流程
-
概述:CAS是一种无锁的原子操作机制,它是实现并发控制的重要手段。在多线程环境下,传统的加锁方式(如使用
synchronized
关键字)会带来线程阻塞、上下文切换等开销,而CAS提供了一种更轻量级的并发处理方式。它的核心思想是在不使用锁的情况下,实现对共享变量的原子更新。 -
基本工作流程:CAS操作涉及三个操作数,分别是内存位置(V)、预期原值(A)和新值(B)。当执行CAS操作时,首先会读取内存位置V中的值,将其与预期原值A进行比较。如果内存位置V中的值与预期原值A相等,就将内存位置V中的值更新为新值B;如果不相等,就不进行更新操作,通常会再次尝试读取和比较,直到满足条件为止。例如,在Java中,
java.util.concurrent.atomic
包下的原子类(如AtomicInteger
)就大量使用了CAS操作来实现原子性的更新操作。以下是一个简单的示例代码:
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(10);
// 预期原值为10,新值为20
boolean success = atomicInteger.compareAndSet(10, 20);
if (success) {
System.out.println("CAS操作成功,新值为: " + atomicInteger.get());
} else {
System.out.println("CAS操作失败");
}
}
}
在这个示例中,compareAndSet
方法就是基于CAS实现的,它尝试将AtomicInteger
的值从10更新为20,如果当前值确实是10(与预期原值相等),则更新成功并返回true
,否则返回false
。
2.4.2 CAS底层实现
在不同的编程语言和操作系统中,CAS的底层实现方式有所不同。以Java为例,在HotSpot虚拟机中,sun.misc.Unsafe
类提供了一些本地方法来实现CAS操作。这些本地方法最终会依赖于底层硬件提供的原子指令。例如,在x86架构的CPU上,通常使用cmpxchg
指令来实现CAS操作。cmpxchg
指令会将目标操作数与累加器(如EAX
寄存器)中的值进行比较,如果相等,就将目标操作数更新为另一个操作数的值。Java通过JNI(Java Native Interface)技术调用这些本地方法,从而在Java层面实现CAS操作。
在Java的AtomicInteger
类中,compareAndSet
方法的实现如下:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这里的unsafe
就是sun.misc.Unsafe
类的实例,compareAndSwapInt
方法会调用底层的本地方法,通过硬件提供的原子指令来完成CAS操作。
2.4.3 乐观锁和悲观锁
-
乐观锁:CAS是乐观锁的一种典型实现。乐观锁的核心思想是假设在大多数情况下,数据不会发生冲突,因此在进行数据更新时,不会像悲观锁那样先对数据进行加锁,而是直接尝试更新。如果在更新过程中发现数据已经被其他线程修改(即CAS操作失败),则采取相应的重试策略或者其他补偿措施。乐观锁适用于读操作比较多、写操作比较少的场景,因为在这种场景下,发生冲突的概率相对较低,使用乐观锁可以减少加锁带来的开销,提高并发性能。
-
悲观锁:与乐观锁相反,悲观锁总是假设最坏的情况,即认为在数据更新时,一定会发生冲突。因此,在对数据进行操作之前,会先获取锁,将数据保护起来,只有持有锁的线程才能对数据进行操作,其他线程则需要等待锁的释放。常见的悲观锁实现有
synchronized
关键字和ReentrantLock
类。悲观锁适用于写操作比较多的场景,因为它可以有效地防止数据冲突,但在高并发情况下,由于线程竞争锁会导致大量的线程阻塞和上下文切换,从而影响性能。
二、volatile
2.5.1 保证线程间的可见性
- 原理:在Java内存模型(JMM)中,每个线程都有自己的工作内存,线程对共享变量的操作是在自己的工作内存中进行的。当一个线程修改了共享变量的值后,不会立即将其写回到主内存中,而是先缓存在工作内存中。这样一来,其他线程读取这个共享变量时,可能读取到的还是旧值,从而导致数据不一致的问题。
volatile
关键字的作用就是解决这个问题,它保证了被volatile
修饰的共享变量在被一个线程修改后,能够立即被其他线程看到最新的值。
具体来说,当一个线程对volatile
修饰的变量进行写操作时,JMM会强制将修改后的值立即写回到主内存中;当其他线程读取这个volatile
变量时,JMM会强制从主内存中读取最新的值,而不是从自己的工作内存中读取缓存的值。例如:
public class VolatileVisibilityExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public void checkFlag() {
while (!flag) {
// 等待flag变为true
}
System.out.println("flag has been set to true");
}
}
在这个示例中,flag
变量被volatile
修饰。当一个线程调用setFlag
方法将flag
设置为true
后,另一个调用checkFlag
方法的线程能够立即感知到flag
的变化,从而跳出循环。
2.5.2 禁止进行指令重排序
-
指令重排序概念:在程序执行过程中,为了提高性能,编译器和处理器可能会对指令进行重新排序。例如,在一段代码中,语句A和语句B在逻辑上没有依赖关系,编译器或处理器可能会将它们的执行顺序进行调整,以更好地利用CPU的流水线等特性。但是,在多线程环境下,这种指令重排序可能会导致程序出现错误的结果。
-
volatile的作用:
volatile
关键字可以禁止指令重排序。它通过在生成的汇编代码中插入内存屏障(Memory Barrier)指令来实现这一功能。内存屏障指令可以确保在它之前的指令先于它之后的指令执行,从而保证了volatile
变量相关操作的执行顺序。例如,对于以下代码:
public class VolatileReorderingExample {
private volatile int num = 0;
private boolean flag = false;
public void write() {
num = 1; // 语句1
flag = true; // 语句2
}
public void read() {
while (!flag) {
// 等待flag变为true
}
// 这里要求先读取到num为1,因为按照逻辑顺序,语句1应该先于语句2执行
System.out.println("num: " + num);
}
}
由于num
和flag
被正确使用了volatile
修饰,在write
方法中,语句1和语句2不会被指令重排序,保证了在read
方法中能够按照预期的顺序读取到数据。
三、AQS(AbstractQueuedSynchronizer,抽象队列同步器)
2.6.1 概述
AQS是Java并发包(java.util.concurrent
)中很多同步工具类(如ReentrantLock
、Semaphore
、CountDownLatch
等)的核心实现基础。它是一个抽象类,提供了一个框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关的同步器。AQS通过一个int类型的状态变量(state
)来表示同步状态,例如在ReentrantLock
中,state
可以用来表示锁被重入的次数;在Semaphore
中,state
可以表示可用的许可数量。同时,AQS维护了一个双向链表结构的等待队列,用于存储等待获取同步状态的线程。
2.6.2 工作机制
- 同步状态的获取与释放:AQS提供了两种方式来获取和释放同步状态,分别是独占式和共享式。
- 独占式:以
ReentrantLock
为例,当一个线程尝试获取独占锁时,会调用AQS的acquire
方法。在acquire
方法中,首先会调用自定义的tryAcquire
方法(需要子类实现)来尝试直接获取同步状态。如果获取成功,就将当前线程设置为同步状态的所有者;如果获取失败,就会将当前线程包装成一个节点加入到等待队列中,并使线程进入阻塞状态。当持有锁的线程释放锁时,会调用release
方法,release
方法会调用自定义的tryRelease
方法(需要子类实现)来尝试释放同步状态。如果释放成功,就会从等待队列中唤醒一个等待的线程。 - 共享式:以
Semaphore
为例,当一个线程尝试获取共享锁(许可)时,会调用AQS的acquireShared
方法。在acquireShared
方法中,同样会先调用自定义的tryAcquireShared
方法(需要子类实现)来尝试获取同步状态。如果获取成功,就直接返回;如果获取失败,也会将当前线程加入到等待队列中。当一个线程释放共享锁时,会调用releaseShared
方法,releaseShared
方法会调用自定义的tryReleaseShared
方法(需要子类实现)来尝试释放同步状态,并且会唤醒等待队列中后续的多个线程(因为是共享式,可能有多个线程可以同时获取到同步状态)。
- 独占式:以
- 等待队列:AQS的等待队列是一个双向链表结构,每个节点代表一个等待获取同步状态的线程。节点包含了线程的引用、前驱节点和后继节点的引用等信息。当一个线程获取同步状态失败时,会被包装成一个节点加入到队列的尾部。队列的头部节点是当前正在尝试获取同步状态或者已经获取到同步状态的节点(如果有的话)。当同步状态释放时,会从队列头部开始唤醒等待的线程。
通过深入理解CAS、volatile和AQS的原理和工作机制,我们能够更好地掌握Java并发编程中的核心技术,从而在实际开发中编写出高效、可靠的多线程程序。无论是利用CAS实现无锁并发,还是通过volatile
保证数据的可见性和防止指令重排序,亦或是基于AQS构建各种同步工具类,都为我们处理多线程场景下的问题提供了强大的工具和方法。