Java锁题库优化
Java锁题库优化
Lock 接口和 synchronized 对比,它有什么优势?
Lock 提供了更灵活、可扩展的锁操作机制,能支持中断响应、定时尝试加锁、公平性策略以及多个条件变量等高级特性,因此相较于 synchronized 更加可控和丰富。
虽然 synchronized 在 JDK1.6 之后性能已大幅提升,但在功能层面仍不如 Lock 灵活。Lock 接口定义了显式加锁/释放的操作,其实现如 ReentrantLock 可以支持公平锁与非公平锁选择,允许线程在获取锁时响应中断,还可以尝试在限定时间内获取锁,避免死等。同时,它还支持多个 Condition 对象,能实现比 synchronized 更复杂的线程协作模型。
举例来说,Lock 接口支持如下操作:
lock.lockInterruptibly(); // 支持响应中断
lock.tryLock(); // 尝试获取锁
lock.tryLock(1, TimeUnit.SECONDS); // 限时等待获取锁
lock.newCondition(); // 创建条件变量
而 synchronized 属于 JVM 内建锁机制,只能非公平加锁、不可中断、不支持条件队列。在需要高并发精细控制的系统中,Lock 通常是更合适的选择。
怎么理解 Lock 与 AQS 的关系?
Lock 是使用者的接口抽象,而 AQS(AbstractQueuedSynchronizer)是锁的实现者底座。AQS 屏蔽了线程排队、阻塞、唤醒等底层细节,大大简化了自定义锁的实现。
从职责角度来看,Lock 是应用层面使用者所依赖的锁功能入口,而 AQS 是支撑锁实现的同步器骨架。通过继承 AQS 并重写指定方法,开发者可以快速构建出具备线程安全控制能力的锁,如 ReentrantLock、ReadWriteLock、CountDownLatch 等。
AQS 本质上提供了一个基于 FIFO 的等待队列和一个 volatile 的状态变量 state 来管理资源竞争。它通过共享/独占两种获取方式,适配了多种同步控制模型。也正因为有了 AQS,像 ReentrantLock 的复杂功能才能被高效实现。
什么是 AQS?
AQS,全称 AbstractQueuedSynchronizer,是 Java 并发包中用于构建锁和同步器的框架。它通过一个 volatile 的状态变量和一个 CLH 队列,实现了线程安全的独占锁与共享锁机制。
AQS 是 J.U.C 中的基础设施组件之一,被广泛用于 ReentrantLock、CountDownLatch、Semaphore、ReadWriteLock 等类的底层实现。它通过一个 int 类型的 state 变量表示当前的同步状态,并使用基于 CLH 的 FIFO 队列管理阻塞线程。
AQS 的核心支持两种访问模式:
- 独占模式(Exclusive):一次只允许一个线程持有锁,如 ReentrantLock;
- 共享模式(Shared):多个线程可以同时访问,如 Semaphore、CountDownLatch。
AQS 的设计思想是:将线程的排队、阻塞、唤醒等通用机制下沉到 AQS,让具体同步器只需关注获取/释放资源的条件逻辑即可,大大简化了并发组件的实现复杂度。
AQS 是怎么实现同步管理的?底层数据结构?
AQS 主要依赖一个 FIFO 等待队列和一个 volatile int 状态变量 state 来实现线程同步管理。
AQS 的核心同步控制逻辑由两部分组成:
- 同步状态 state:用于表示资源是否可用。一般 state=0 表示无锁,>0 表示有线程占用;支持重入时 state 叠加。
- 同步队列(CLH 队列):使用双向链表存储所有阻塞等待的线程,节点类型为内部类 Node,包含线程引用、前驱、后继等信息。
当线程获取锁失败后,就会被封装成 Node 节点并加入同步队列,随后通过 LockSupport.park()
被挂起,直到前驱节点释放锁再唤醒自己。
这种队列式同步结构确保了线程访问的有序性,并避免了忙等带来的资源浪费。此外,AQS 提供了独占/共享两种模式,只需通过不同的 tryAcquire/tryRelease 方法组合即可实现各种类型的锁组件。
AQS 有哪些核心方法?
AQS 提供了三类关键方法:状态控制方法、需要子类实现的核心方法、模板方法。通过这些组合,开发者可以快速实现高性能同步器。
AQS 方法体系分为三大类:
状态管理方法(3个):
getState()
、setState(int)
、compareAndSetState(int, int)
,用于原子控制同步状态state
。需子类重写的核心方法(5个):
tryAcquire(int)
、tryRelease(int)
、tryAcquireShared(int)
、tryReleaseShared(int)
、isHeldExclusively()
,是实现独占/共享语义的关键逻辑。模板方法(内部实现):封装了队列挂起、唤醒、CAS 重试等流程,调用上层的 try 方法决定线程是否可继续执行。
一个完整的同步器实现,通常只需继承 AQS 并重写上述核心方法即可,其他线程管理细节都由 AQS 提供的模板方法处理,极大提高了同步器开发效率与性能。
ReentrantLock 和 synchronized 的对比?
两者都可实现线程间同步和可见性保障,但 ReentrantLock 提供了更丰富的特性,如可中断锁获取、定时尝试、条件队列以及公平性控制。相比之下,synchronized 更简洁,但缺乏灵活性。
synchronized 是 JVM 内建的语法结构,属于重量级同步工具,底层由 JVM 保证线程安全。它使用方便,编译期即确定了加锁范围,但特性较少,不支持中断等待、限时获取等能力。而 ReentrantLock 是基于 AQS 实现的可重入互斥锁,提供如下增强能力:
- 响应中断:lockInterruptibly() 可处理中断信号,防止死等;
- 限时加锁:tryLock(timeout) 可在超时后放弃锁;
- 公平性控制:构造时可选择公平锁或非公平锁;
- 条件队列支持:通过 Condition 对象实现精准唤醒和等待。
虽然在性能上两者已接近,但 ReentrantLock 在复杂并发场景中更具可操作性,尤其适合对锁控制精细、需要条件等待机制的业务场景。
什么是可重入?什么是可重入锁?
可重入指的是同一个线程在持有某个锁的情况下可以再次获取该锁,而不会被自己阻塞。可重入锁都支持这种机制,避免了死锁问题。
可重入锁是一种支持递归加锁的机制。在某个线程持有该锁的情况下,如果它再次进入加锁代码区域,不会被阻塞,而是会累加一个计数器,表示锁被获取的层数。每次 unlock 或退出同步块,计数器减一,直到计数为 0,锁才真正释放。
synchronized 由 JVM 内部支持重入;ReentrantLock 则通过内部计数器 state 记录锁的获取次数,两者都保证了可重入性。例如:
public synchronized void methodA() {
methodB(); // 可重入
}
若没有可重入特性,将导致同一线程阻塞自身,进而死锁。因此,这是多线程锁机制的基本要求之一。
公平锁和非公平锁有什么区别?
公平锁遵循先到先得原则,线程按请求顺序排队获取锁;非公平锁允许插队,新来的线程可能抢在排队线程前获得锁,虽然效率高但可能造成饿死。
公平锁通过维护 FIFO 队列保证获取顺序,避免线程饥饿问题。例如:
new ReentrantLock(true); // 公平锁
而非公平锁不保证顺序,更倾向于让刚到的线程立即尝试 CAS 获取锁,提升系统吞吐量。区别在于实现中是否判断 hasQueuedPredecessors()
。
公平锁的缺点是会产生频繁的上下文切换,而非公平锁在高并发下更能提升性能,但可能导致一些线程长期得不到锁,尤其在非平均任务执行时间下。
为什么非公平锁比公平锁性能更好?
非公平锁采用直接尝试 + 入队等待的策略,减少了排队线程被频繁唤醒和调度的次数,因此上下文切换更少,系统吞吐量更高。
非公平锁执行流程如下:
- 线程先直接使用 CAS 尝试获取锁;
- 若失败,再进入 AQS 同步队列等待;
这种方式允许插队,大大减少了线程切换带来的内核态与用户态转换开销。相反,公平锁每次都唤醒队头线程获取锁,即使新线程已准备就绪,也必须排队等待,导致整体响应变慢。
这也是为什么高性能场景,如 Netty、Disruptor,几乎都采用非公平策略的原因之一。
ReentrantLock 是如何实现公平锁和非公平锁的?
ReentrantLock 内部通过继承自 AQS 的 FairSync 和 NonfairSync 两个子类实现公平与非公平逻辑,其核心差异体现在 tryAcquire()
方法对队列中前驱线程的判断。
ReentrantLock 的结构如下:
ReentrantLock
持有Sync
抽象类引用;NonfairSync
是默认实现,先尝试抢锁(不判断队列);FairSync
实现中会先检查hasQueuedPredecessors()
,确保排队线程优先。
非公平锁的实现逻辑如下:
protected final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0 && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
// 其他情况进入阻塞队列
}
公平锁则会先判断是否存在前驱节点:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 同样进入 AQS 队列
}
由此可见,公平性控制完全由 tryAcquire 中是否检查同步队列决定,是 ReentrantLock 灵活性的核心体现。
ReentrantReadWriteLock 是什么?
ReentrantReadWriteLock 是 Java 并发包中的可重入读写锁,实现了 ReadWriteLock 接口。它提供了两把锁:ReadLock(共享锁) 和 WriteLock(独占锁),允许多个线程并发读,但写操作是互斥的。
与 ReentrantLock 不同,ReentrantReadWriteLock 提供了更细粒度的并发控制能力:读操作可以并发,而写操作必须独占。在读多写少的场景中,可以大幅提升并发性能。
- ReadLock 是共享锁,多个线程可同时持有;
- WriteLock 是独占锁,同一时刻只能被一个线程持有;
- 它是可重入锁,读锁和写锁都支持重入;
- 允许写锁降级为读锁,但不允许读锁升级为写锁。
底层实现依然依赖 AQS,不同之处在于对高低位 state 的拆分控制,读锁与写锁通过 state 的不同位进行标识与计数,线程安全且高效。
共享锁和独占锁有什么区别?
共享锁允许多个线程同时获取锁资源,适合并发读场景;独占锁在任意时刻只允许一个线程访问临界区,适合写场景或存在修改操作的流程。
Java 中的 ReentrantReadWriteLock 明确区分了这两种锁语义:
- 共享锁(如 ReadLock):并发性能高,适用于大量读少量写的业务场景;
- 独占锁(如 WriteLock、ReentrantLock):保证修改时的数据一致性,牺牲并发性以换取线程间的独占访问。
使用共享锁时,必须确保对共享资源的访问不会造成冲突或副作用;否则即使允许多个线程访问,也可能导致数据竞争。
线程持有读锁还能获取写锁吗?
不能。线程如果已经持有读锁,再尝试获取写锁会失败。这是为了避免升级引起死锁。相反,若线程先获取写锁,再尝试获取读锁是允许的,即写锁可重入读锁。
ReentrantReadWriteLock 明确规定:
1.读锁 → 写锁(升级)不被支持。即使读锁是当前线程持有,只要有其他线程持有读锁,当前线程也无法获取写锁。
2.写锁 → 读锁(降级)是允许的,并且是线程安全的。
原因在于,如果多个线程都持有读锁并尝试升级写锁,将陷入互相等待的死锁。例如:
lock.readLock().lock();
// 无法直接升级为写锁,会失败甚至死锁
lock.writeLock().lock(); // 可能阻塞
推荐策略是先释放所有读锁,再尝试加写锁,或者改为通过锁降级实现读写互斥逻辑。
什么是锁的升降级?ReentrantReadWriteLock 为什么不支持锁升级?
锁的升降级指线程在持有某种锁的情况下尝试获取另一种锁。ReentrantReadWriteLock 支持写锁降级为读锁,但不支持读锁升级为写锁,这是为了避免线程间竞争导致死锁。
锁的升降级关系如下:
1.写锁降级为读锁:线程先持有写锁,再获取读锁,然后释放写锁,典型应用是写后立即读;
2.读锁升级为写锁:线程已持有读锁,再尝试获取写锁,该行为 ReentrantReadWriteLock 不支持。
主要原因是:升级过程会引起多个线程的写锁竞争。如果两个线程都持有读锁并尝试升级,都会等待对方释放读锁,导致死锁。
正确用法是:
writeLock.lock();
try {
// 修改操作
readLock.lock(); // 写锁降级为读锁
try {
// 后续读操作
} finally {
readLock.unlock();
}
} finally {
writeLock.unlock();
}
ReentrantReadWriteLock 底层读写状态如何设计的?
ReentrantReadWriteLock 通过一个整型变量 state 的高 16 位表示读锁数量,低 16 位表示写锁持有状态,实现了对读写状态的分离与并发控制。
ReentrantReadWriteLock 继承自 AQS,其 state 状态变量被拆分为:
1.高 16 位:记录共享读锁的持有数量;
2.低 16 位:记录独占写锁的重入次数(最多 65535 次);
状态拆解公式:
int sharedCount = state >>> 16; // 读锁计数
int exclusiveCount = state & 0xFFFF; // 写锁计数
通过位运算区分锁类型,实现了读写互斥、写写互斥、读读共享等并发控制逻辑。这种设计兼顾效率与内存开销,也是 ReadWriteLock 得以实现高并发读场景的关键。
