java多线程与juc并发编程,深入底层,搞定java面试,升级加薪,Java多线程JUC并发编程

81 阅读9分钟

90.jpg

java多线程与juc并发编程,深入底层,搞定java面试,升级加薪,Java多线程JUC并发编程-----夏の哉---97it.---top/---14104/

Java 并发包(JUC)详解:Lock、Condition 与 AQS 底层原理

在 Java 并发编程中,synchronized关键字曾是实现线程同步的主要方式,但它存在灵活性不足(无法中断等待、不能超时获取锁)、功能有限(仅支持单一条件队列)等问题。Java 并发包(JUC)引入的Lock接口及其实现(如ReentrantLock),配合Condition和底层的 AQS(AbstractQueuedSynchronizer),构建了更强大的同步机制。理解这三者的底层原理,是掌握 Java 高级并发编程的核心。

一、AQS:同步器的 “基础设施”

AQS 是 JUC 中大多数同步工具的底层骨架,包括Lock、Semaphore、CountDownLatch等。它通过 “状态管理 + 队列调度” 实现线程的同步与协作,核心思想是 “基于状态的排队机制”

1. 核心结构:状态与双向队列

AQS 的内部维护两个核心要素:

  • 同步状态(state) :一个volatile int变量,用于表示资源的占用情况(如state=0表示锁空闲,state=1表示被占用,state>1表示重入次数)。
  • 双向同步队列(CLH 队列) :当线程获取资源失败时,会被包装成节点(Node) 加入队列,等待被唤醒。节点包含线程引用、等待状态(如CANCELLED、SIGNAL)、前驱 / 后继指针。

CLH 队列是一种 “虚拟双向队列”(节点间通过指针关联,无实际容器存储),结构如下:

head(头节点,哨兵) ←→ Node1(线程A) ←→ Node2(线程B) ←→ ... ←→ tail(尾节点)
  • 头节点是 “哨兵节点”(不关联线程),表示当前持有锁的线程;
  • 新节点通过compareAndSetTail原子操作加入队尾,保证线程安全。

2. 核心操作:获取与释放资源

AQS 通过模板方法模式定义了获取和释放资源的流程,子类需重写tryAcquire(独占式获取)、tryRelease(独占式释放)等方法。

独占式获取资源(如ReentrantLock的lock()):
  1. 调用tryAcquire(arg):子类实现具体的获取逻辑(如state是否为 0,是则 CAS 修改为 1);
  1. 若成功,当前线程直接返回;
  1. 若失败,将线程包装为 Node 加入 CLH 队列,调用LockSupport.park()阻塞线程;
  1. 当持有资源的线程释放后,会唤醒队列中的后继节点,重复步骤 1。
独占式释放资源(如ReentrantLock的unlock()):
  1. 调用tryRelease(arg):子类实现释放逻辑(如state减 1,直至 0 表示完全释放);
  1. 若释放后状态允许(如state=0),唤醒 CLH 队列中的首个等待节点(通过LockSupport.unpark())。

3. 核心设计:模板方法与钩子函数

AQS 定义了一系列模板方法(如acquire、release),封装了队列管理、线程阻塞 / 唤醒等通用逻辑,而将具体的资源获取 / 释放逻辑委托给子类(通过重写钩子函数):

  • 必须重写的方法:tryAcquire、tryRelease(独占式),tryAcquireShared、tryReleaseShared(共享式);
  • 可选重写的方法:isHeldExclusively(判断是否独占持有)。

这种设计让 AQS 能适配不同的同步场景(如独占锁、共享锁),同时避免代码重复。

二、Lock 接口:比 synchronized 更灵活的锁

Lock接口是 JUC 对 “锁” 的抽象,定义了获取锁、释放锁、中断等待等操作,弥补了synchronized的不足。其核心实现类ReentrantLock(可重入锁)就是基于 AQS 实现的。

1. Lock 接口的核心方法

public interface Lock {
    void lock(); // 获取锁,若失败则阻塞
    void lockInterruptibly() throws InterruptedException; // 可被中断的获取
    boolean tryLock(); // 尝试获取锁,立即返回结果(非阻塞)
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时获取
    void unlock(); // 释放锁
    Condition newCondition(); // 创建条件对象
}

与synchronized相比,Lock的优势在于:

  • 可中断:lockInterruptibly()允许线程在等待时响应中断(如Thread.interrupt());
  • 超时机制:tryLock(time)避免线程无限等待;
  • 多条件队列:通过newCondition()创建多个Condition,实现更精细的线程协作。

2. ReentrantLock 的实现:基于 AQS 的独占锁

ReentrantLock通过内部类Sync(继承 AQS)实现锁逻辑,Sync有两个子类:

  • NonfairSync(非公平锁,默认):线程获取锁时直接尝试抢占,可能插队;
  • FairSync(公平锁):线程严格按 CLH 队列顺序获取锁,不允许插队。
非公平锁的lock()流程:
  1. 调用compareAndSetState(0, 1):尝试直接抢占锁(state从 0→1);
  1. 若成功,设置当前线程为持有者(setExclusiveOwnerThread(Thread.currentThread()));
  1. 若失败,调用acquire(1)进入 AQS 的获取流程:
    • tryAcquire(1):检查state,若为 0 则再次尝试 CAS;若当前线程是持有者,则state+1(重入);
    • 若仍失败,线程入队并阻塞。
释放锁的unlock()流程:
  1. 调用release(1),最终执行tryRelease(1):
    • state减 1,若减为 0,清空持有者线程;
    • 返回true表示完全释放,唤醒队列中的线程。

3. 可重入性的实现

ReentrantLock的 “可重入” 特性(同一线程可多次获取锁)通过state计数实现:

  • 首次获取:state从 0→1;
  • 再次获取(同一线程):state+1(如state=2);
  • 释放时:每次unlock()对应state-1,直至state=0才完全释放锁。

三、Condition:基于 AQS 的条件队列

Condition是Lock的 “配套工具”,用于实现线程间的条件等待(类似synchronized中的wait()/notify()),但支持多个独立的条件队列(synchronized仅一个)。

1. Condition 与 Object 的等待 / 通知对比

特性ConditionObject的wait()/notify()
关联锁与Lock绑定与synchronized绑定
条件队列数量多个(每个Condition对应一个队列)1 个(与锁对象关联)
唤醒方式signal()(唤醒一个)、signalAll()(唤醒全部)notify()、notifyAll()
中断响应支持(awaitInterruptibly())支持(被中断时抛出InterruptedException)
超时等待支持(await(time))支持(wait(time))

2. Condition 的底层实现:条件队列与 AQS 的协作

Condition的实现类是ConditionObject(ReentrantLock的内部类),其底层依赖 AQS 的条件队列锁状态

核心结构:
  • 条件队列:一个单向链表,存储调用await()的线程(节点类型与 AQS 的 Node 相同);
  • 锁关联:Condition必须与Lock绑定,调用await()前必须持有锁。
await()流程(线程等待):
  1. 检查当前线程是否持有锁,若未持有则抛出IllegalMonitorStateException;
  1. 将当前线程包装为 Node 加入条件队列;
  1. 释放锁(调用release(state),让其他线程有机会获取锁);
  1. 阻塞当前线程(LockSupport.park()),等待被signal()唤醒;
  1. 唤醒后,重新竞争获取锁(调用acquire(state));
  1. 若成功获取,从await()返回;若失败,继续阻塞。
signal()流程(唤醒线程):
  1. 检查当前线程是否持有锁;
  1. 取出条件队列的首节点,将其转移到 AQS 的同步队列(CLH 队列);
  1. 唤醒该节点关联的线程(LockSupport.unpark()),使其有机会竞争锁。

3. 与 synchronized 的 wait/notify 对比优势

  • 多条件队列:例如,一个线程池的Lock可创建两个Condition(notEmpty和notFull),分别管理 “队列非空” 和 “队列未满” 的等待线程,避免synchronized中单一队列的 “唤醒风暴”(notifyAll()唤醒所有无关线程);
  • 精准控制:Condition的signal()仅唤醒一个线程,signalAll()唤醒所有,而synchronized的notify()随机唤醒一个,难以精准控制。

四、AQS、Lock、Condition 的协同关系

三者的关系可概括为:AQS 是底层骨架,Lock 是 AQS 的独占式实现,Condition 是基于 AQS 的条件等待机制

1. 层级关系

  • 底层:AQS 提供状态管理、队列调度、线程阻塞 / 唤醒等基础能力;
  • 中层:Lock(如ReentrantLock)通过继承 AQS 并重写tryAcquire/tryRelease,实现独占锁的语义;
  • 上层:Condition依赖Lock的 AQS 实例,利用其条件队列和同步队列,实现线程的条件等待与唤醒。

2. 协作流程(以生产者 - 消费者模型为例)

  1. 生产者获取Lock(lock()),若缓冲区满,调用notFull.await()进入条件队列;
  1. 消费者获取Lock,取出数据后调用notFull.signal(),唤醒生产者线程;
  1. 生产者线程从条件队列转移到同步队列,竞争锁;
  1. 成功获取锁后,生产者写入数据,调用notEmpty.signal()唤醒消费者,最后释放锁(unlock())。

这一过程中:

  • AQS 的同步队列管理锁的竞争;
  • Condition的条件队列管理线程的条件等待;
  • Lock的获取与释放贯穿全程,确保操作的原子性。

五、实战与面试要点

1. 常见错误用法

  • 未持有锁时调用 Condition 方法:await()/signal()必须在lock()和unlock()之间调用,否则抛出IllegalMonitorStateException;
  • 忘记释放锁:lock()后未在finally中调用unlock(),可能导致死锁;
  • 混淆公平锁与非公平锁:非公平锁性能更好(减少线程切换),但可能导致线程饥饿;公平锁保证顺序,但性能略低。

2. 面试高频问题

  • AQS 的 state 变量为什么用 volatile?

保证state的可见性(一个线程修改后,其他线程能立即看到),配合 CAS 操作(compareAndSetState)实现原子性,确保同步状态的线程安全。

  • ReentrantLock 与 synchronized 的区别?
特性ReentrantLocksynchronized
锁获取方式显式调用lock()/unlock()隐式(代码块 / 方法)
可中断性支持(lockInterruptibly())不支持(等待时不可中断)
超时机制支持(tryLock(time))不支持
公平性可选择(公平 / 非公平)非公平
条件队列多个(Condition)1 个
性能高并发下更优(减少内核态切换)JDK1.6 后优化,性能接近,但灵活性低
  • Condition 的 await () 为什么要释放锁?

若不释放锁,其他线程无法获取锁,导致signal()永远无法被调用,等待的线程会永久阻塞。释放锁是为了让其他线程有机会满足条件并唤醒等待线程,实现协作。

结语:JUC 同步机制的设计哲学

AQS、Lock、Condition 的设计体现了 “分层抽象” 与 “模板方法” 的思想:

  • AQS 将同步的共性逻辑(队列、阻塞)抽象出来,通过钩子函数适配不同场景;
  • Lock 接口定义了锁的行为规范,具体实现委托给 AQS 的子类;
  • Condition 则基于 AQS 的扩展能力,实现更灵活的线程协作。

这种设计让 JUC 的同步工具既统一又灵活,既避免重复开发,又能满足多样化的并发需求。理解它们的底层原理,不仅能更高效地使用ReentrantLock等工具,更能在复杂场景下设计自定义同步工具(如基于 AQS 实现分布式锁)。对于 Java 开发者而言,这是从 “会用 API” 到 “理解设计思想” 的关键一跃。