在Java并发编程中,synchronized 作为JVM内置锁,虽简单易用,但在复杂并发场景(如公平锁、可中断锁、超时获取锁、多条件唤醒)中灵活性不足。JDK 5 引入的 java.util.concurrent.locks 包(简称JUC锁),以 Lock 接口为核心,ReentrantLock 为主要实现,底层依托 AQS(抽象队列同步器)CAS(比较并交换) 两大核心技术,实现了更灵活、高效的同步机制。

本文将从「使用场景→API细节→底层原理→源码解析→实战避坑」,全方位拆解 Lock 体系,不仅讲清“是什么”“怎么用”,更深入剖析“为什么这么设计”,帮你彻底掌握Java并发锁的核心逻辑,应对面试高频考点和实际开发难题。

一、Lock 接口:JDK层面的锁规范(比synchronized更灵活)

Lock 是JDK提供的顶层锁接口,定义了锁的核心操作规范,区别于 synchronized(JVM内置,隐式操作),Lock 是显式锁,需手动控制锁的获取与释放,同时提供了 synchronized 不具备的高级特性。

1.1 Lock 接口完整API

方法 核心作用 使用场景 注意事项
void lock() 获取锁,若锁被占用则阻塞,直到获取到锁 基础同步场景,无需中断、超时 不可中断,若线程被阻塞,只能等待锁释放
void lockInterruptibly() throws InterruptedException 获取锁,可响应线程中断,中断时抛出异常 避免线程永久阻塞(如超时任务、可取消任务) 必须捕获 InterruptedException,处理中断逻辑
boolean tryLock() 尝试获取锁,非阻塞,立即返回结果(成功true/失败false) 无锁竞争时快速获取,避免阻塞 获取失败不阻塞,需手动判断是否重试
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 超时获取锁,在指定时间内未获取到则返回false,支持中断 防止线程无限等待(如网络请求、资源获取) 时间单位需明确,中断时抛出异常
void unlock() 释放锁,必须手动调用 所有获取锁的场景,必写操作 必须在 finally 中调用,防止锁泄漏;未获取锁时调用抛异常
Condition newCondition() 创建条件变量,实现线程的精准唤醒 多条件等待/唤醒(如生产者-消费者模型) Condition 的 await()/signal() 需在锁范围内调用

1.2 Lock 与 synchronized 深度对比

除了基础特性对比,补充二者在底层实现、性能、适用场景的核心差异,帮你精准选择:

特性 synchronized Lock(ReentrantLock) 补充说明
锁实现层面 JVM 内置(C++ 底层实现,依赖对象头) JDK 层面(Java 代码实现,依赖 AQS) synchronized 由JVM自动管理锁生命周期,Lock 需手动管理
释放方式 自动释放(方法退出、代码块结束、异常抛出) 手动释放(必须在 finally 中调用 unlock()) Lock 忘记释放会导致锁泄漏,synchronized 无此问题
中断性 不可中断(阻塞线程无法被中断,只能等待锁释放) 支持可中断(lockInterruptibly()) Lock 可避免线程永久阻塞,更适合高可用场景
超时获取 不支持(无超时机制,线程可能无限等待) 支持(tryLock 超时重载方法) Lock 可设置超时时间,提升系统稳定性
公平锁支持 仅非公平锁(无法指定) 支持公平/非公平锁(构造函数指定) 公平锁需维护队列顺序,性能略低,非特殊场景不推荐
条件变量 单一条件(依赖 wait()/notify()/notifyAll()) 多条件(多个 Condition 实例,精准唤醒) synchronized 的 notifyAll() 会唤醒所有等待线程,Lock 可唤醒指定条件的线程
锁状态查询 不支持(无法判断锁是否被持有、重入次数) 支持(isLocked()、getHoldCount()、isFair() 等) Lock 可灵活查询锁状态,便于调试和监控
性能 Java 6 后优化(偏向锁、轻量级锁),与 Lock 接近 高并发下略优,尤其是非公平锁场景 简单场景二者性能差异可忽略,复杂场景 Lock 更灵活
适用场景 简单同步场景(如单一线程安全变量修改) 复杂并发场景(公平锁、可中断、超时、多条件唤醒) 优先使用 synchronized(简单不易出错),复杂场景用 Lock

1.3 Lock 接口的实现类(扩展)

Lock 接口并非只有 ReentrantLock 一个实现,还有其他常用实现类,覆盖不同场景:

  • ReentrantLock:可重入独占锁(最常用),支持公平/非公平模式,本文重点讲解;

  • ReentrantReadWriteLock.ReadLock/WriteLock:可重入读写锁,读锁共享、写锁独占,适合读多写少场景;

  • StampedLock:JDK 8 新增,优化读写锁性能,支持乐观读模式,解决读写锁的饥饿问题;

  • LockSupport:不是 Lock 接口实现,但提供了 park()/unpark() 方法,是 AQS 阻塞/唤醒线程的底层依赖。

二、ReentrantLock:Lock 接口的核心实现(可重入锁详解)

ReentrantLock 意为“可重入锁”,是 Lock 接口最核心、最常用的实现类,其核心特性与 synchronized 类似(可重入),但提供了更灵活的控制能力。底层基于 AQS 实现,支持公平锁和非公平锁两种模式。

2.1 ReentrantLock 核心构造函数

ReentrantLock 有两个核心构造函数,用于指定锁的公平性,底层通过 AQS 的子类实现:

// 1. 默认构造函数:非公平锁(性能优先)
public ReentrantLock() {
    sync = new NonfairSync(); // 非公平锁同步器
}

// 2. 带参数构造函数:指定公平/非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync(); // 公平/非公平同步器
}

关键细节:

  • sync 是 ReentrantLock 的内部类,继承自 AQS,分为 FairSync(公平锁同步器)和NonfairSync(非公平锁同步器);

  • 公平锁和非公平锁的核心差异,在于 tryAcquire(int arg) 方法的实现(AQS 的模板方法,由子类实现)。

2.2 ReentrantLock 基础使用

2.2.1 基本用法(非公平锁,最常用)

核心要点:lock() 获取锁,finally 中 unlock() 释放锁,避免锁泄漏。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockBasicDemo {
    // 1. 创建非公平可重入锁(默认)
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    // 同步方法:修改共享变量
    public void increment() {
        // 2. 获取锁(阻塞式)
        lock.lock();
        try {
            // 临界区代码:保证原子性、可见性、有序性
            count++;
            System.out.println(Thread.currentThread().getName() + ":count=" + count);
            // 模拟临界区耗时操作
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢复中断状态
        } finally {
            // 3. 释放锁(必须在finally中,无论是否异常)
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockBasicDemo demo = new ReentrantLockBasicDemo();
        // 10个线程,每个执行10次increment,测试线程安全
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    demo.increment();
                }
            }, "线程" + (i+1)).start();
        }
    }
}

2.2.2 公平锁用法(场景:避免线程饥饿)

公平锁的核心是“先到先得”,线程获取锁时,会先检查同步队列是否有等待线程,有则入队,无则尝试获取锁。适合对“公平性”要求高的场景(如金融交易、任务调度)。

public class ReentrantLockFairDemo {
    // 构造函数指定公平锁
    private final ReentrantLock fairLock = new ReentrantLock(true);
    private int count = 0;

    public void increment() {
        fairLock.lock();
        try {
            count++;
            System.out.println(Thread.currentThread().getName() + ":count=" + count + ",是否公平锁:" + fairLock.isFair());
        } finally {
            fairLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockFairDemo demo = new ReentrantLockFairDemo();
        // 3个线程竞争锁,观察执行顺序(公平锁会按线程启动顺序执行)
        new Thread(() -> {
            for (int i = 0; i < 3; i++) demo.increment();
        }, "公平线程1").start();

        new Thread(() -> {
            for (int i = 0; i < 3; i++) demo.increment();
        }, "公平线程2").start();

        new Thread(() -> {
            for (int i = 0; i < 3; i++) demo.increment();
        }, "公平线程3").start();
    }
}

执行结果特点:线程会按启动顺序依次获取锁,不会出现某个线程频繁抢锁的情况(非公平锁可能出现)。

2.2.3 可中断获取锁(场景:取消耗时任务)

lockInterruptibly() 允许线程在获取锁的过程中被中断,避免线程永久阻塞。例如:用户取消任务时,中断正在等待锁的线程。

public class ReentrantLockInterruptDemo {
    private final ReentrantLock lock = new ReentrantLock();

    // 可中断获取锁的方法
    public void doTask() throws InterruptedException {
        // 若线程被中断,会抛出InterruptedException
        lock.lockInterruptibly();
        try {
            System.out.println(Thread.currentThread().getName() + " 获取锁成功,执行任务");
            // 模拟耗时任务
            Thread.sleep(5000);
        } finally {
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + " 释放锁");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockInterruptDemo demo = new ReentrantLockInterruptDemo();

        // 线程1:获取锁后执行耗时任务
        Thread t1 = new Thread(() -> {
            try {
                demo.doTask();
            } catch (InterruptedException e) {
                System.out.println("线程1被中断,停止执行任务");
            }
        }, "任务线程1");

        // 线程2:尝试获取锁,然后中断线程1
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(1000); // 等待1秒,让t1先获取锁
                t1.interrupt(); // 中断t1
                System.out.println("线程2中断线程1");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "中断线程2");

        t1.start();
        t2.start();
    }
}

执行结果:线程1获取锁后执行任务,线程2中断线程1,线程1抛出异常并释放锁,避免永久阻塞。

2.2.4 超时获取锁(场景:防止线程无限等待)

tryLock(long time, TimeUnit unit) 允许线程在指定时间内尝试获取锁,超时未获取则返回false,适合网络请求、资源获取等场景(避免因资源不可用导致线程卡死)。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTimeoutDemo {
    private final ReentrantLock lock = new ReentrantLock();

    // 超时获取锁的方法
    public boolean tryDoTask(long timeout) {
        try {
            // 超时1秒获取锁,失败则返回false
            if (lock.tryLock(timeout, TimeUnit.SECONDS)) {
                try {
                    System.out.println(Thread.currentThread().getName() + " 获取锁成功,执行任务");
                    Thread.sleep(2000); // 模拟任务耗时
                    return true;
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " 获取锁超时,放弃执行");
                return false;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    public static void main(String[] args) {
        ReentrantLockTimeoutDemo demo = new ReentrantLockTimeoutDemo();

        // 线程1:先获取锁,执行2秒任务
        new Thread(() -> demo.tryDoTask(1), "线程1").start();

        // 线程2:1秒后尝试获取锁,超时时间1秒(此时线程1还在执行,线程2超时)
        new Thread(() -> {
            try {
                Thread.sleep(1000);
                demo.tryDoTask(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "线程2").start();
    }
}

2.2.5 多条件唤醒(Condition 用法,场景:生产者-消费者)

ReentrantLock 的 newCondition() 方法可创建多个条件变量,实现线程的精准唤醒,解决 synchronized 中 notifyAll() 唤醒所有线程的低效问题。

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

// 生产者-消费者模型:用 Condition 精准唤醒
public class ReentrantLockConditionDemo {
    private final ReentrantLock lock = new ReentrantLock();
    // 两个条件变量:生产者等待(队列满)、消费者等待(队列空)
    private final Condition producerCondition = lock.newCondition();
    private final Condition consumerCondition = lock.newCondition();
    private final Queue<Integer> queue = new LinkedList<>();
    private final int MAX_CAPACITY = 5; // 队列最大容量

    // 生产者:生产数据,队列满则等待
    public void produce(int data) throws InterruptedException {
        lock.lock();
        try {
            // 队列满,生产者等待
            while (queue.size() == MAX_CAPACITY) {
                System.out.println("队列满,生产者等待");
                producerCondition.await(); // 释放锁,进入等待状态
            }
            // 生产数据
            queue.offer(data);
            System.out.println("生产者生产:" + data + ",队列大小:" + queue.size());
            // 唤醒消费者(队列有数据了)
            consumerCondition.signal();
        } finally {
            lock.unlock();
        }
    }

    // 消费者:消费数据,队列空则等待
    public int consume() throws InterruptedException {
        lock.lock();
        try {
            // 队列空,消费者等待
            while (queue.isEmpty()) {
                System.out.println("队列空,消费者等待");
                consumerCondition.await(); // 释放锁,进入等待状态
            }
            // 消费数据
            int data = queue.poll();
            System.out.println("消费者消费:" + data + ",队列大小:" + queue.size());
            // 唤醒生产者(队列有空闲了)
            producerCondition.signal();
            return data;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockConditionDemo demo = new ReentrantLockConditionDemo();

        // 3个生产者线程
        for (int i = 0; i < 3; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    for (int j = 0; j < 3; j++) {
                        demo.produce(finalI * 10 + j);
                        Thread.sleep(500);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "生产者" + (i+1)).start();
        }

        // 2个消费者线程
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 5; j++) {
                        demo.consume();
                        Thread.sleep(800);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "消费者" + (i+1)).start();
        }
    }
}

关键优势:生产者等待时,仅唤醒消费者;消费者等待时,仅唤醒生产者,避免不必要的线程唤醒,提升性能。

2.3 ReentrantLock 核心特性(深入解析)

2.3.1 可重入性

可重入性是指:同一线程获取锁后,可再次获取同一锁(无需释放),底层通过 AQS 的 state 变量记录重入次数。

public class ReentrantLockReentrantDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void method1() {
        lock.lock();
        try {
            System.out.println("进入method1,重入次数:" + lock.getHoldCount());
            method2(); // 同一线程重复获取锁
        } finally {
            lock.unlock();
            System.out.println("退出method1,重入次数:" + lock.getHoldCount());
        }
    }

    public void method2() {
        lock.lock();
        try {
            System.out.println("进入method2,重入次数:" + lock.getHoldCount());
            method3(); // 再次重入
        } finally {
            lock.unlock();
            System.out.println("退出method2,重入次数:" + lock.getHoldCount());
        }
    }

    public void method3() {
        lock.lock();
        try {
            System.out.println("进入method3,重入次数:" + lock.getHoldCount());
        } finally {
            lock.unlock();
            System.out.println("退出method3,重入次数:" + lock.getHoldCount());
        }
    }

    public static void main(String[] args) {
        ReentrantLockReentrantDemo demo = new ReentrantLockReentrantDemo();
        demo.method1();
        // 输出:
        // 进入method1,重入次数:1
        // 进入method2,重入次数:2
        // 进入method3,重入次数:3
        // 退出method3,重入次数:2
        // 退出method2,重入次数:1
        // 退出method1,重入次数:0
    }
}

底层原理(结合 AQS):

  • AQS 的 state 变量(volatile int)记录锁的重入次数,初始值为 0;

  • 线程第一次获取锁:CAS 将 state 从 0 改为 1,设置 exclusiveOwnerThread 为当前线程;

  • 线程再次获取锁:判断当前线程是否为exclusiveOwnerThread,若是则 state+1(重入次数增加);

  • 线程释放锁:state-1,当 state=0 时,清空 exclusiveOwnerThread,真正释放锁。

2.3.2 公平锁与非公平锁的底层差异(源码对比)

公平锁和非公平锁的核心差异,在于 tryAcquire(int arg) 方法的实现(AQS 的模板方法,由 ReentrantLock 的内部类实现)。

1. 非公平锁(NonfairSync)的 tryAcquire 实现
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    // 尝试获取独占锁
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

// 非公平锁获取逻辑(核心)
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 1. 若锁未被持有(state=0),直接CAS抢锁
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 2. 若当前线程已持有锁,重入(state+1)
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 3. 锁被其他线程持有,返回false(入队等待)
    return false;
}

非公平锁逻辑:线程尝试获取锁时,先直接 CAS 抢锁(不管队列中是否有等待线程),抢锁失败再入队,性能更高,但可能导致线程饥饿(某些线程一直抢不到锁)。

2. 公平锁(FairSync)的 tryAcquire 实现
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 关键差异:先检查队列是否有等待线程,无则CAS抢锁
            if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 重入逻辑与非公平锁一致
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

// 检查同步队列中是否有前驱线程(是否有线程比当前线程先等待)
public final boolean hasQueuedPredecessors() {
    Node t = tail; // 队列尾节点
    Node h = head; // 队列头节点
    Node s;
    // 头节点 != 尾节点 → 队列非空
    // 队列头节点的后继节点 != 当前线程 → 有前驱线程
    return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

公平锁逻辑:线程尝试获取锁时,先通过 hasQueuedPredecessors() 检查同步队列是否有等待线程,有则入队,无则 CAS 抢锁,保证“先到先得”,无线程饥饿,但性能略低(需维护队列顺序)。

三、底层核心:AQS(抽象队列同步器)深度解析

AQS(AbstractQueuedSynchronizer)是 JUC 锁的“骨架”,是所有 JUC 锁(ReentrantLock、CountDownLatch、Semaphore 等)的底层基础。AQS 封装了锁的核心逻辑(锁获取、锁释放、线程阻塞/唤醒、同步队列管理),开发者只需实现少量抽象方法,即可自定义锁。

AQS 的核心设计思想:基于状态变量(state)+ 同步队列(CLH队列)+ CAS 操作,实现锁的独占/共享获取与释放。

3.1 AQS 核心结构(源码级拆解)

3.1.1 核心变量(AQS 类的核心字段)

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    // 序列化版本号
    private static final long serialVersionUID = 7373984972572414691L;

    // 同步队列的头节点(CLH队列)
    private transient volatile Node head;

    // 同步队列的尾节点(CLH队列)
    private transient volatile Node tail;

    // 锁状态变量:volatile修饰,保证可见性和有序性
    private volatile int state;

    // 构造函数(空参)
    protected AbstractQueuedSynchronizer() {}

    // 其他核心方法...
}

关键变量详解:

  • state:volatile int 类型,核心状态变量,不同锁的含义不同:

  • ReentrantLock(独占锁):state=0 表示未锁定,state>0 表示已锁定(值为锁的重入次数);

  • Semaphore(共享锁):state 表示可用许可数;

  • CountDownLatch(共享锁):state 表示剩余计数,countDown() 减1,await() 等待 state=0。

head/tail:volatile Node 类型,指向同步队列(CLH队列)的头节点和尾节点,CLH队列是一个双向链表,用于存储等待获取锁的线程。

Node 节点:AQS 的内部类,封装了等待线程、等待状态、前驱/后继节点,是同步队列的基本单元。

3.1.2 Node 节点结构(核心)

static final class Node {
    // 共享模式标记
    static final Node SHARED = new Node();
    // 独占模式标记
    static final Node EXCLUSIVE = null;

    // 节点状态:等待状态,用于控制线程的阻塞/唤醒
    volatile int waitStatus;
    // 状态值:
    // 1. CANCELLED = 1:线程被取消(如超时、中断),从队列中移除
    // 2. SIGNAL = -1:当前节点的后继节点需要被唤醒
    // 3. CONDITION = -2:线程在条件变量上等待(Condition.await())
    // 4. PROPAGATE = -3:共享模式下,唤醒会传播给后续节点
    // 5. 0:默认状态(无特殊含义)

    // 前驱节点
    volatile Node prev;

    // 后继节点
    volatile Node next;

    // 当前节点对应的线程
    volatile Thread thread;

    // 条件变量相关的节点(Condition 用法)
    Node nextWaiter;

    // 其他方法...
}

3.1.3 AQS 核心模式(独占/共享)

AQS 支持两种核心模式,对应不同的锁场景:

1. 独占模式(Exclusive Mode)

同一时间仅一个线程能获取锁,其他线程需入队等待。适用于“排他性访问”场景(如 ReentrantLock、ReentrantReadWriteLock 的写锁)。

核心方法(模板方法,由子类实现):

  • tryAcquire(int arg):尝试获取独占锁,成功返回true,失败返回false;

  • tryRelease(int arg):尝试释放独占锁,成功返回true,失败返回false;

  • isHeldExclusively():判断当前线程是否持有独占锁。

2. 共享模式(Shared Mode)

多个线程可同时获取锁,只要锁的状态允许(如 Semaphore 的许可数>0)。适用于“共享访问”场景(如 CountDownLatch、Semaphore、ReentrantReadWriteLock 的读锁)。

核心方法(模板方法,由子类实现):

  • tryAcquireShared(int arg):尝试获取共享锁,返回>=0 表示成功(返回值为剩余许可数),返回<0 表示失败;

  • tryReleaseShared(int arg):尝试释放共享锁,成功返回true,失败返回false;

  • doAcquireShared(int arg):共享模式下,获取锁失败后入队等待;

  • doReleaseShared(int arg):共享模式下,释放锁后唤醒后续节点。

3.2 AQS 核心流程(以 ReentrantLock 独占锁为例,完整链路)

AQS 的核心流程分为“锁获取”和“锁释放”两大环节,结合 CAS 和同步队列,实现线程的阻塞与唤醒。

3.2.1 锁获取流程(lock() 方法调用链路)

ReentrantLock.lock() → AQS.acquire(1) → 子类 tryAcquire(1) → 入队阻塞 → 唤醒后再次尝试获取锁。

// 1. ReentrantLock.lock() 调用(非公平锁为例)
public void lock() {
    sync.lock(); // 调用 NonfairSync.lock()
}

// 2. NonfairSync.lock() 实现
final void lock() {
    // 先CAS抢锁(非公平锁特性)
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 抢锁失败,调用AQS的acquire方法
        acquire(1);
}

// 3. AQS.acquire(int arg) 模板方法(核心)
public final void acquire(int arg) {
    // 步骤1:尝试获取锁(tryAcquire由子类实现)
    // 步骤2:获取失败,封装为Node节点入队(addWaiter)
    // 步骤3:阻塞当前线程(acquireQueued)
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt(); // 若线程被中断,恢复中断状态
}

关键步骤拆解:

  1. tryAcquire(arg):子类实现(ReentrantLock 的公平/非公平逻辑),尝试获取锁,成功则返回true,失败则进入下一步;

  2. addWaiter(Node.EXCLUSIVE):将当前线程封装为独占模式的 Node 节点,通过 CAS 加入同步队列尾部(保证入队原子性);

  3. 若队列为空(head/tail 为null),先初始化队列(创建头节点);

  4. 若队列非空,CAS 将新节点设为尾节点,同时将原尾节点的 next 指向新节点。

  5. acquireQueued(Node node, int arg):让节点在队列中等待,直到获取到锁或被中断;

  6. 循环检查当前节点的前驱节点是否为头节点,若是则再次尝试获取锁;

  7. 若获取锁成功,将当前节点设为头节点,释放原头节点;

  8. 若获取锁失败,根据前驱节点的状态,决定是否阻塞当前线程(LockSupport.park());

  9. 线程被唤醒后,再次进入循环,尝试获取锁。

AQS 独占锁获取流程图(核心链路)

抢锁成功(state=0→1)

抢锁失败/公平锁有等待线程

成功(重入/首次获取)

失败

成功

失败

线程调用lock()

非公平锁先CAS抢锁
公平锁先检查队列

设置exclusiveOwnerThread为当前线程

调用AQS.acquire(1)模板方法

tryAcquire(arg)尝试获取锁(子类实现公平/非公平逻辑)

addWaiter(Node.EXCLUSIVE)创建独占节点

队列是否为空?

初始化队列,创建头节点

CAS将新节点设为尾节点

acquireQueued(Node node, arg)循环等待

当前节点前驱是头节点?

再次尝试获取锁

将当前节点设为头节点,释放原头节点

根据前驱节点状态,阻塞线程(LockSupport.park())

线程被唤醒(unpark())

执行临界区代码

流程图说明:整个流程围绕“尝试获取→失败入队→阻塞等待→唤醒重试”展开,CAS保证入队和锁状态修改的原子性,volatile保证state和队列节点的可见性,最终实现线程的安全同步。

3.2.2 锁释放流程(unlock() 方法调用链路)

锁释放流程相对简单,核心是“释放锁状态→唤醒后续等待线程”,ReentrantLock.unlock() → AQS.release(1) → 子类 tryRelease(1) → 唤醒队列中的后继线程。

// 1. ReentrantLock.unlock() 调用
public void unlock() {
    sync.release(1); // 调用AQS的release方法
}

// 2. AQS.release(int arg) 模板方法(核心)
public final boolean release(int arg) {
    // 步骤1:尝试释放锁(tryRelease由子类实现)
    if (tryRelease(arg)) {
        // 步骤2:释放成功,获取头节点
        Node h = head;
        // 步骤3:头节点不为空且状态不是0,唤醒后继节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // 唤醒后继线程
        return true;
    }
    return false;
}

// 3. ReentrantLock的tryRelease实现(Sync类中)
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 只有持有锁的线程才能释放锁
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 重入次数减为0,真正释放锁
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null); // 清空持有线程
    }
    setState(c); // 更新state状态
    return free;
}

// 4. 唤醒后继节点(AQS核心方法)
private void unparkSuccessor(Node node) {
    // 获取当前节点状态
    int ws = node.waitStatus;
    // 若状态为SIGNAL(-1),重置为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 找到后继节点(跳过已取消的节点)
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从尾节点向前找,找到最前面未取消的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 唤醒后继节点的线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

关键步骤拆解:

  1. tryRelease(arg):子类实现,核心是减少 state 重入次数,当 state=0 时,清空独占线程,标志锁完全释放;

  2. unparkSuccessor(h):找到头节点的后继节点(跳过已取消的节点),通过 LockSupport.unpark() 唤醒该线程,被唤醒的线程会再次进入 acquireQueued 循环,尝试获取锁;

  3. 锁释放的核心原则只有持有锁的线程才能释放锁,且必须释放到 state=0 才算真正释放,避免锁泄漏。

AQS 独占锁释放流程图(核心链路)

失败(非持有锁线程)

成功

否(重入未释放完)

是(完全释放)

线程调用unlock()

调用AQS.release(1)

tryRelease(1)释放锁(子类实现)

抛出IllegalMonitorStateException异常

state是否减为0?

更新state,返回false,不唤醒线程

清空exclusiveOwnerThread

获取头节点h

h不为空且h.waitStatus≠0?

返回true,释放完成

unparkSuccessor(h)唤醒后继节点

找到未取消的后继节点,调用unpark()唤醒

被唤醒线程进入循环,尝试获取锁

返回true,释放完成

3.3 AQS 核心设计亮点(面试高频)

AQS 之所以能成为 JUC 锁的核心骨架,其设计亮点主要体现在以下3点,也是面试中常考的核心考点:

  1. 模板方法模式:AQS 封装了锁的通用逻辑(入队、阻塞、唤醒、状态管理),定义了抽象方法(tryAcquire、tryRelease 等),由子类(ReentrantLock 的 Sync 内部类)实现具体的锁逻辑,降低了自定义锁的开发成本,同时保证了锁的核心一致性;

  2. CAS + volatile 保证线程安全:state 变量用 volatile 修饰,保证可见性和有序性;队列节点的入队、状态修改用 CAS 操作,保证原子性,避免了 synchronized 的阻塞开销,提升了高并发场景下的性能;

  3. CLH 双向队列高效管理等待线程:CLH 队列是一种双向链表,采用“先进先出”(FIFO)原则,避免线程饥饿(公平锁),同时通过节点状态(SIGNAL、CANCELLED 等)过滤无效节点,减少不必要的线程唤醒,提升效率。

四、底层基石:CAS(比较并交换)深度解析

CAS(Compare And Swap,比较并交换)是 AQS、ReentrantLock 等并发组件的底层原子操作,是实现“无锁编程”的核心技术,也是解决并发安全问题的关键,其核心思想是“无锁竞争”,比 synchronized 更轻量。

4.1 CAS 核心定义与原理

CAS 是一种原子操作(不可中断、不可分割),其操作逻辑如下:

给定三个参数:内存地址 V、预期值 A、新值 B;

当且仅当内存地址 V 中的值等于预期值 A 时,将 V 中的值更新为 B,否则不做任何操作;

无论操作成功与否,都返回 V 中原来的值。

简单来说:“我认为内存里的值是 A,要是真的是 A,就改成 B,不然就不改”,整个过程原子完成,无需加锁。

4.1.1 CAS 底层实现(JDK + CPU 层面)

CAS 并非 Java 层面的实现,而是依赖 CPU 的原子指令(不同 CPU 指令不同):

  • x86 架构:CPU 提供 cmpxchg 指令(compare and exchange),实现 CAS 原子操作;

  • Java 层面:通过 sun.misc.Unsafe 类调用 native 方法,底层调用 CPU 原子指令;

  • 核心 API:Unsafe 类中的 compareAndSwapInt(Object o, long offset, int expected, int update),分别对应“对象、内存偏移量、预期值、新值”。

// AQS 中 CAS 修改 state 的核心代码(简化)
private final boolean compareAndSetState(int expect, int update) {
    // 调用 Unsafe 的 CAS 方法,修改 state 变量
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

关键细节:stateOffset 是 state 变量在 AQS 类中的内存偏移量,通过 Unsafe 类的 objectFieldOffset 方法获取,确保能精准定位到 state 变量的内存地址。

4.2 CAS 的优势与缺陷

4.2.1 优势

  • 无锁竞争,性能高效:CAS 是无锁操作,无需阻塞线程,减少了线程上下文切换的开销(synchronized 阻塞线程会导致上下文切换),高并发场景下性能优于 synchronized;

  • 原子性保证:依赖 CPU 原子指令,确保操作不可分割,避免并发安全问题;

  • 轻量级:无需 JVM 层面的锁管理(如 synchronized 的偏向锁、轻量级锁切换),底层直接操作内存,开销小。

4.2.2 缺陷(面试高频考点)

  1. ABA 问题
    定义:内存地址 V 中的值从 A 改成 B,再改回 A,CAS 操作会认为“值未变”,从而执行更新操作,可能导致并发安全问题;

  2. 解决方式:使用“版本号机制”(如 AtomicStampedReference),给每个值关联一个版本号,更新时不仅比较值,还比较版本号,避免 ABA 问题。

  3. 自旋开销
    定义:CAS 操作失败时,线程会循环重试(自旋),直到操作成功;若高并发下大量线程自旋,会占用 CPU 资源,导致 CPU 使用率飙升;

  4. 解决方式:设置自旋次数上限(如 AQS 中的自旋次数限制),或搭配阻塞机制(自旋失败后阻塞线程),平衡性能和 CPU 开销。

  5. 只能保证单个变量的原子操作:CAS 只能对单个变量执行原子操作,无法实现多个变量的原子性(如同时修改两个变量);

  6. 解决方式:使用 AtomicReference 包装多个变量(如自定义对象),或使用 synchronized 保证多个变量的原子性。

4.3 CAS 在 AQS 中的应用(核心场景)

AQS 中大量使用 CAS 操作,核心应用场景有3个:

  1. 修改 state 变量:如 ReentrantLock 中,CAS 将 state 从 0 改为 1(获取锁)、从 1 改为 0(释放锁),保证锁状态修改的原子性;

  2. 同步队列节点入队:addWaiter 方法中,CAS 将新节点设为尾节点,保证多个线程同时入队时的原子性,避免队列混乱;

  3. 修改节点状态:如 unparkSuccessor 方法中,CAS 将节点的 waitStatus 从 SIGNAL(-1)改为 0,保证节点状态修改的原子性。

五、实战避坑:ReentrantLock、AQS、CAS 常见问题与解决方案

结合实际开发和面试高频问题,整理核心避坑点,帮你规避并发bug,加深对底层原理的理解。

5.1 ReentrantLock 常见坑

坑1:忘记在 finally 中释放锁,导致锁泄漏

现象:线程获取锁后,因异常或逻辑遗漏未释放锁,导致其他线程永久阻塞,最终引发系统死锁;

解决方案:必须在 finally 块中调用 unlock() 方法,无论是否发生异常,都能保证锁释放;

// 错误示例(无finally释放锁)
public void wrongMethod() {
    lock.lock();
    // 临界区代码,若抛出异常,锁无法释放
    count++;
    lock.unlock(); // 异常后不会执行
}

// 正确示例(finally释放锁)
public void rightMethod() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock(); // 无论是否异常,必执行
    }
}

坑2:公平锁滥用,导致性能下降

现象:为了“公平性”,盲目使用公平锁,导致线程频繁入队等待,吞吐量下降;

解决方案:非特殊场景(如金融交易、任务调度),优先使用默认的非公平锁,平衡性能和公平性;

核心原因:公平锁需每次检查同步队列,增加了额外开销,非公平锁的“抢锁”机制更高效。

坑3:多线程调用 unlock(),导致 IllegalMonitorStateException

现象:未获取锁的线程调用 unlock(),抛出异常;或同一线程多次释放锁(超过重入次数);

解决方案:

  • 确保只有获取锁的线程才能调用 unlock();

  • 使用 getHoldCount() 方法查询重入次数,避免多次释放。

5.2 AQS 常见问题(面试高频)

问题1:AQS 的 state 变量为什么用 volatile 修饰?

解答:保证 state 的可见性和有序性:

  • 可见性:一个线程修改 state 后,其他线程能立即看到最新值,避免因缓存导致的锁状态不一致;

  • 有序性:禁止指令重排序,确保 state 的修改和读取顺序符合代码逻辑,避免“锁已释放但线程仍阻塞”的问题。

问题2:AQS 的同步队列为什么是双向链表?

解答:双向链表便于节点的删除和唤醒操作:

  • 删除节点:当线程被取消(CANCELLED 状态),可通过前驱节点快速移除当前节点,避免队列冗余;

  • 唤醒后继:释放锁时,需找到头节点的后继节点,双向链表可快速定位前驱和后继,提升唤醒效率。

问题3:AQS 的 park() 和 unpark() 与 Thread.sleep() 的区别?

解答:核心区别在于“是否释放锁”和“唤醒机制”:

  • LockSupport.park():阻塞线程时,不会释放已获取的锁,可被 unpark() 直接唤醒,也可被中断唤醒;

  • Thread.sleep(long ms):阻塞线程时,不会释放锁,只能等待超时或被中断唤醒,无法被主动唤醒;

  • 应用场景:AQS 中用 park()/unpark() 实现线程的阻塞与唤醒,因为可主动唤醒,更灵活,适合锁的等待场景。

5.3 CAS 常见问题(面试高频)

问题1:ABA 问题的具体场景的解决方案?

示例场景:线程1准备将变量从 A 改为 B,线程2先将 A 改为 C,再改回 A,线程1 CAS 操作认为值未变,执行修改,导致数据不一致;

解决方案:使用 AtomicStampedReference,给变量关联版本号,更新时同时比较值和版本号:

// AtomicStampedReference 解决 ABA 问题
AtomicStampedReference<Integer> atomicStamped = new AtomicStampedReference<>(1, 1); // 初始值1,版本号1

// 线程1尝试更新:预期值1,新值2,预期版本号1,新版本号2
boolean success = atomicStamped.compareAndSet(1, 2, 1, 2);
// 若线程2修改了值和版本号,线程1的CAS会失败

问题2:CAS 自旋次数过多导致 CPU 飙升,如何解决?

解决方案:

  • 设置自旋次数上限:如 AQS 中,自旋一定次数(如10次)后,若仍未获取锁,就阻塞线程,避免无限自旋;

  • 搭配阻塞机制:自旋失败后,调用 LockSupport.park() 阻塞线程,减少 CPU 占用;

  • 使用自适应自旋:根据前一次自旋的结果,动态调整自旋次数(如前一次成功,增加自旋次数;前一次失败,减少自旋次数)。

六、总结:Lock 体系核心逻辑梳理(面试必背)

本文从 Lock 接口入手,逐步拆解 ReentrantLock、AQS、CAS 的底层原理,核心逻辑可总结为“三层架构”:

  1. 上层:Lock 接口:定义锁的规范,提供灵活的锁操作(可中断、超时、多条件唤醒),是开发者直接使用的入口;

  2. 中层:ReentrantLock:Lock 接口的核心实现,基于 AQS 实现公平/非公平锁、可重入性,封装了锁的具体逻辑;

  3. 底层:AQS + CAS:AQS 是锁的骨架,封装了队列管理、线程阻塞/唤醒、状态管理;CAS 是原子操作基础,保证锁状态和队列操作的原子性,二者共同支撑起整个 Lock 体系。

核心考点提炼:

  • ReentrantLock 的公平/非公平锁差异、可重入性原理;

  • AQS 的核心结构(state、CLH队列、Node节点)、核心流程(锁获取/释放);

  • CAS 的原理、优势、缺陷及解决方案;

  • Lock 与 synchronized 的对比,适用场景选择。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐