面试题答案
一键面试AQS实现线程同步与互斥的原理
- 核心数据结构
- CLH队列:AQS使用一个FIFO的双向队列(CLH队列,Craig, Landin, and Hagersten队列)来管理等待获取同步状态的线程。队列的每个节点(Node)包含了线程的引用、等待状态等信息。每个新的等待线程会被封装成一个Node节点加入到队列尾部。
- 同步状态(state):通过一个int类型的变量来表示同步状态。不同的同步器对state的含义有不同的定义。例如,ReentrantLock中,state表示锁的持有次数;在CountDownLatch中,state表示计数。
- 同步状态的管理
- 获取同步状态:当线程调用
acquire
方法时,会尝试获取同步状态。以ReentrantLock为例,tryAcquire
方法会检查当前state是否为0(表示锁未被持有),如果是则尝试通过CAS操作将state设置为1,并将当前线程设置为锁的持有者。如果state不为0且当前线程是锁的持有者,则将state加1(实现可重入)。如果获取失败,线程会被封装成Node节点加入到CLH队列尾部等待。 - 释放同步状态:当线程调用
release
方法时,会尝试释放同步状态。还是以ReentrantLock为例,tryRelease
方法会将state减1。如果state减为0,表示锁已完全释放,会唤醒CLH队列头部节点的后继节点对应的线程。
- 获取同步状态:当线程调用
- 队列操作与内存可见性之间的关系
- 可见性保证:根据Java内存模型(JMM),volatile变量具有可见性和有序性保证。AQS中的同步状态state被声明为volatile,这确保了对state的修改对其他线程是可见的。当一个线程修改了state,其他线程能够立即看到这个变化。
- 队列操作与可见性:在CLH队列操作中,新节点的入队(
enq
方法)和节点状态的更新等操作,通过使用Unsafe类的CAS操作来保证原子性。这些操作在保证数据一致性的同时,也依赖于JMM的内存屏障机制来保证内存可见性。例如,当一个线程通过CAS操作将新节点入队时,这个操作会附带内存屏障效果,确保之前对共享变量(如state)的修改对后续线程可见。
高并发场景下AQS的优化
- 减少竞争
- 使用读写锁(ReadWriteLock):在高并发读多写少的场景下,使用读写锁可以提高并发性能。读操作可以并发执行,而写操作则需要独占锁。AQS框架提供了
ReentrantReadWriteLock
实现了读写锁功能。 - 分段锁:对于一些需要保护多个独立资源的场景,可以使用分段锁。例如,在
ConcurrentHashMap
中,使用多个Segment(每个Segment都是一个小型的HashTable),每个Segment有自己的锁,这样不同Segment的操作可以并发进行,减少锁竞争。
- 使用读写锁(ReadWriteLock):在高并发读多写少的场景下,使用读写锁可以提高并发性能。读操作可以并发执行,而写操作则需要独占锁。AQS框架提供了
- 优化队列操作
- 减少队列长度:通过合理的资源分配和使用策略,尽量减少等待获取同步状态的线程数量,从而缩短CLH队列的长度。例如,对于一些短期的同步操作,可以考虑使用自旋锁(在AQS的
acquire
方法中,部分实现会有自旋的逻辑),避免线程直接进入队列等待,减少线程上下文切换的开销。 - 优化唤醒策略:在唤醒等待线程时,采用更智能的唤醒策略。例如,
Condition
接口提供了更灵活的线程等待和唤醒机制,可以根据业务需求唤醒特定条件的线程,而不是像Object
的notifyAll
那样唤醒所有等待线程,从而减少不必要的线程竞争和上下文切换。
- 减少队列长度:通过合理的资源分配和使用策略,尽量减少等待获取同步状态的线程数量,从而缩短CLH队列的长度。例如,对于一些短期的同步操作,可以考虑使用自旋锁(在AQS的
- 使用非阻塞数据结构
- 在某些情况下,可以结合非阻塞数据结构来替代传统的基于锁的数据结构。例如,
ConcurrentLinkedQueue
是一个线程安全的非阻塞队列,在不需要严格同步的场景下,可以使用它来提高并发性能,减少对AQS同步机制的依赖。
- 在某些情况下,可以结合非阻塞数据结构来替代传统的基于锁的数据结构。例如,