在 Java 并发编程领域,同步机制是保障线程安全的核心,而我们最熟悉的莫过于 synchronized 关键字。但自 JDK 5 引入 java.util.concurrent.locks.Lock 接口及其实现类后,Java 并发编程的灵活性得到了质的提升。Lock 接口凭借“显式控制”“可中断”“公平性配置”“多条件通信”等独有的特性,完美弥补了 synchronized 的局限性,成为复杂并发场景(如高并发调度、多线程协作)的首选方案。

本文将从 Lock 接口的核心设计入手,深入拆解 ReentrantLock(可重入锁)、ReentrantReadWriteLock(读写分离锁)的底层原理与核心特性,全面对比 Lock 与 synchronized 的差异,结合实战案例讲解如何正确使用 Lock 解决并发问题、规避死锁,同时补充面试高频考点,助力开发者吃透 Lock 机制,从容应对面试与生产实战。

一、Lock 接口:同步机制的抽象定义,打破 synchronized 的局限

Lock 接口是 Java 并发包对“锁”机制的抽象封装,它将锁的“获取”与“释放”解耦为独立的方法,摆脱了 synchronized 对代码块、方法的绑定,为开发者提供了更精细的控制能力。与 synchronized 的“隐式同步”不同,Lock 采用“显式同步”,开发者需手动控制锁的获取与释放,这既是灵活性的体现,也对编码严谨性提出了更高要求。

1.1 核心方法深度解析(附使用场景)

Lock 接口的核心方法定义了锁的基本操作,每个方法都对应特定的并发场景,理解其细节是正确使用 Lock 的关键,以下结合场景逐一解析:

方法 功能描述 关键特性 适用场景
void lock() 获取锁,若锁已被其他线程占用,则当前线程阻塞,直到获取到锁 不可中断,与 synchronized 的阻塞逻辑一致,无超时机制 简单同步场景,无需中断、无需超时,仅需基础互斥
void lockInterruptibly() throws InterruptedException 获取锁,若锁被占用则阻塞,但允许线程在等待过程中响应中断(如调用 Thread.interrupt() 可中断,中断后抛出 InterruptedException,线程可放弃等待 需要取消任务的场景(如用户取消操作、任务超时),避免线程无限阻塞
boolean tryLock() 尝试获取锁,无论成功与否立即返回结果,不阻塞线程 非阻塞,成功返回 true,失败返回 false,无等待时间 无需等待锁的场景,如“尝试获取锁,失败则执行备用逻辑”
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 在指定时间内尝试获取锁,超时未获取则返回 false,等待过程中可响应中断 结合超时与可中断特性,兼顾灵活性与安全性 避免死锁的核心场景(如多锁竞争),防止线程无限等待
void unlock() 释放锁,将锁归还给锁池,供其他线程竞争 必须手动调用,且需放在 finally 块中,否则会导致锁泄漏 所有 Lock 使用场景的必备操作,确保锁在任何情况下都能释放
Condition newCondition() 创建一个条件变量,用于线程间的协作通信(类似 synchronized 的wait()/notify() 支持多条件等待,可精准唤醒指定条件的线程,避免无效唤醒 多线程协作场景(如生产者-消费者、任务调度)

核心设计思想:Lock 接口的本质是“将锁的操作标准化、灵活化”——它不绑定具体的代码结构,允许开发者根据业务需求,灵活选择锁的获取方式(阻塞、非阻塞、超时、可中断),同时通过条件变量实现更精细的线程协作,这也是它与 synchronized 最核心的区别。

1.2 与 synchronized 的本质区别(面试高频)

很多面试中会问到“Lock 与 synchronized 的区别”,除了表面的“显式/隐式”,更核心的差异体现在底层实现、控制粒度和功能扩展上,以下是全面对比(结合底层原理):

  • 获取与释放的显式性: synchronized 是隐式同步,线程进入 synchronized 代码块/方法时自动获取锁,退出时(正常退出或异常退出)自动释放锁,无需手动干预; Lock 是显式同步,必须手动调用 lock()(或其重载方法)获取锁,调用 unlock() 释放锁,且 unlock() 必须放在 finally 块中,否则会导致锁泄漏(线程异常退出时锁未释放,其他线程无法获取)。
  • 灵活性: synchronized 仅支持“不可中断、无超时、非公平”的锁获取,功能单一,无法满足复杂场景需求; Lock 支持可中断、超时获取、公平/非公平锁配置、多条件通信,灵活性极高,可适配各种复杂并发场景。
  • 底层实现: synchronized 是 JVM 层面的实现(底层由 C++ 代码编写,依赖操作系统的互斥量 Mutex),锁升级(偏向锁→轻量级锁→重量级锁)由 JVM 自动完成; Lock 是 Java 代码层面的实现(基于 AQS 框架——AbstractQueuedSynchronizer,队列同步器),锁的逻辑由 Java 代码控制,可自定义扩展(如自定义锁实现)。
  • 性能: 低并发、无竞争场景下,两者性能接近(synchronized 经过 JDK 6 优化后,性能大幅提升); 高并发、高竞争场景下,Lock(如 ReentrantLock)性能更稳定,因为它避免了 synchronized 升级为重量级锁后的内核态切换开销,且支持更灵活的锁策略。

补充面试点:Lock 不能完全替代 synchronized——synchronized 是 JVM 层面的同步,可被 JVM 优化(如锁消除、锁粗化),且无需手动释放锁,不易出错;而 Lock 需手动控制锁的释放,编码不当易导致死锁或锁泄漏,因此需根据场景选择。

二、ReentrantLock:可重入锁的经典实现,Lock 接口的核心落地

ReentrantLock 是 Lock 接口最常用、最经典的实现类,其名称中的“Reentrant”(可重入)是它的核心特性——即同一个线程可以多次获取同一把锁,这与 synchronized 的可重入性一致,保证了线程在重入同步代码时不会死锁。

ReentrantLock 底层基于 AQS 框架实现,通过“状态变量(state)”记录线程获取锁的次数,实现可重入性;通过“等待队列”管理阻塞的线程,实现公平/非公平锁的控制,是复杂并发场景中最常用的锁实现。

2.1 基本使用方法(必掌握,避坑关键)

ReentrantLock 的使用必须遵循“获取 - 使用 - 释放”的固定模式,核心是“unlock() 必须放在finally 块中”——无论临界区代码是否抛出异常,都能确保锁被释放,避免锁泄漏。

public class ReentrantLockDemo {
    // 1. 创建ReentrantLock实例(默认非公平锁,可传入true创建公平锁)
    private final Lock lock = new ReentrantLock();
    // 共享资源(需同步保护)
    private int count = 0;

    // 同步方法:增加计数器
    public void increment() {
        // 2. 获取锁(可根据场景选择lock()、lockInterruptibly()等方法)
        lock.lock();
        try {
            // 3. 临界区操作(修改共享资源)
            count++;
            System.out.println("当前计数器:" + count + ",当前线程:" + Thread.currentThread().getName());
        } finally {
            // 4. 释放锁:必须放在finally块中,确保锁一定被释放
            lock.unlock();
        }
    }

    // 同步方法:获取计数器
    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        // 多线程测试
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2; j++) {
                    demo.increment();
                }
            }, "线程" + (i+1)).start();
        }
    }
}

关键注意事项(避坑重点):

  1. 切勿忘记调用 unlock():若临界区代码抛出异常,且 unlock() 未放在 finally 块中,锁会被当前线程永久持有,其他线程无法获取,导致死锁;
  2. 可重入性的正确使用:同一线程多次调用 lock() 后,必须调用相同次数unlock() 才能完全释放锁(如调用 2 次 lock(),需调用 2 次 unlock());
  3. Lock 实例建议用 final 修饰:避免 Lock 实例被修改(如赋值为 null),导致锁失效,引发线程安全问题。

2.2 核心特性详解(底层+实战,面试重点)

ReentrantLock 的核心特性的是它优于 synchronized 的关键,也是面试高频考点,以下结合底层实现和实战示例,逐一拆解。

2.2.1 可重入性(底层原理+示例)

可重入性:同一个线程可以多次获取同一把锁,无需担心死锁。例如,线程 A 获取锁后,再次调用需要同一把锁的方法,可直接获取锁,无需阻塞。

底层实现原理:ReentrantLock 内部通过 AQS 的 state 变量(int 类型)记录线程获取锁的次数:

  • 线程首次获取锁时,将 state 从 0 改为 1,并记录当前持有锁的线程 ID;
  • 线程再次获取锁时,判断当前线程 ID 与持有锁的线程 ID 一致,将 state 加 1;
  • 线程释放锁时,将 state 减 1,当 state 变为 0 时,锁才真正释放,供其他线程竞争。

实战示例(可重入性验证):

public class ReentrantDemo {
    // 非公平锁实例
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        // 线程首次获取锁
        lock.lock();
        try {
            System.out.println("第一次获取锁,state = 1");
            // 线程再次获取锁(可重入)
            lock.lock();
            try {
                System.out.println("第二次获取锁,state = 2");
                // 可继续重入,state 继续加 1
                lock.lock();
                try {
                    System.out.println("第三次获取锁,state = 3");
                } finally {
                    lock.unlock(); // 第三次释放,state = 2
                }
            } finally {
                lock.unlock(); // 第二次释放,state = 1
            }
        } finally {
            lock.unlock(); // 第一次释放,state = 0,锁真正释放
        }
    }
}

运行结果:依次输出“第一次获取锁”“第二次获取锁”“第三次获取锁”,无阻塞,验证了可重入性。若不可重入,线程第二次获取锁时会阻塞,导致死锁。

2.2.2 公平性控制(公平锁 vs 非公平锁)

ReentrantLock 支持两种锁模式,通过构造函数指定,这是它与 synchronized(仅非公平锁)的核心区别之一:

// 1. 非公平锁(默认):线程获取锁的顺序不保证与请求顺序一致,允许“插队”
Lock nonFairLock = new ReentrantLock();

// 2. 公平锁:线程获取锁的顺序与请求顺序一致,先请求的线程先获取(FIFO)
Lock fairLock = new ReentrantLock(true);

公平性的底层权衡(面试重点):

  • 公平锁: 实现原理:通过 AQS 的“等待队列”维护线程的请求顺序,新请求的线程会加入队列尾部,只有队列头部的线程才能获取锁; 优势:避免线程饥饿(某些线程长期无法获取锁),保证调度公平性; 劣势:性能较差——需要维护队列的顺序,频繁切换线程,增加开销。
  • 非公平锁: 实现原理:新请求的线程会先尝试“插队”获取锁(直接 CAS 修改 state),若失败再加入等待队列; 优势:性能更好——避免了队列维护和线程切换的开销,适合高并发场景; 劣势:可能导致线程饥饿(某些线程长期插队,其他线程无法获取锁)。

适用场景:

  • 公平锁:对公平性要求高的场景(如资源调度系统、任务队列调度),需保证每个线程都能公平获取资源;
  • 非公平锁:追求高性能的一般场景(如计数器、缓存更新),默认选择即可,非公平锁的性能通常比公平锁高 10%~20%。

2.2.3 可中断的锁获取(解决无限阻塞问题)

synchronized 的锁获取是“不可中断”的——一旦线程阻塞在 synchronized 代码块前,除非持有锁的线程释放锁,否则该线程会一直阻塞,无法响应中断。而 ReentrantLock 的 lockInterruptibly() 方法,允许线程在等待锁的过程中响应中断,避免无限期阻塞。

实战示例(可中断锁的使用):

public class InterruptibleLockDemo {
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        // 线程1:尝试获取可中断锁
        Thread t1 = new Thread(() -> {
            try {
                // 可中断地获取锁:等待过程中若被中断,会抛出InterruptedException
                lock.lockInterruptibly();
                try {
                    System.out.println("线程1获取到锁,开始执行耗时操作");
                    Thread.sleep(1000); // 模拟耗时操作
                } finally {
                    lock.unlock(); // 确保锁释放
                    System.out.println("线程1释放锁");
                }
            } catch (InterruptedException e) {
                // 捕获中断异常,线程放弃获取锁,执行后续逻辑
                System.out.println("线程1被中断,放弃获取锁");
                e.printStackTrace();
            }
        }, "线程1");

        // 主线程先获取锁,让线程1阻塞等待
        lock.lock();
        t1.start();
        Thread.sleep(200); // 让线程1进入阻塞等待状态
        t1.interrupt(); // 中断线程1的等待
        lock.unlock(); // 主线程释放锁(即使线程1被中断,也要释放锁,避免锁泄漏)
    }
}

运行结果解析:

线程1调用 lockInterruptibly() 后,因主线程持有锁而阻塞;主线程调用 t1.interrupt() 后,线程1被中断,抛出 InterruptedException,执行 catch 块逻辑,放弃获取锁,避免了无限阻塞。

应用场景:任务取消、超时控制(如用户取消正在执行的任务,线程可中断等待锁的过程,快速释放资源)。

2.2.4 超时获取锁(避免死锁的核心手段)

ReentrantLock 的 tryLock(long time, TimeUnit unit) 方法,允许线程在指定时间内尝试获取锁:若在超时时间内获取到锁,返回 true;若超时未获取到锁,返回 false,线程可放弃等待,执行备用逻辑,这是避免死锁的核心手段。

实战示例(超时获取锁):

public class TimeoutLockDemo {
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        // 线程1:尝试在1秒内获取锁
        Thread t1 = new Thread(() -> {
            try {
                // 尝试在1秒内获取锁,超时则返回false
                if (lock.tryLock(1, TimeUnit.SECONDS)) {
                    try {
                        System.out.println("线程1获取到锁,开始执行");
                        Thread.sleep(2000); // 模拟持有锁2秒
                    } finally {
                        lock.unlock();
                        System.out.println("线程1释放锁");
                    }
                } else {
                    // 超时未获取到锁,执行备用逻辑
                    System.out.println("线程1超时(1秒),未获取到锁");
                }
            } catch (InterruptedException e) {
                System.out.println("线程1获取锁时被中断");
                e.printStackTrace();
            }
        }, "线程1");

        // 主线程先获取锁,持有1.5秒
        lock.lock();
        t1.start();
        Thread.sleep(1500); // 主线程持有锁1.5秒,超过线程1的超时时间(1秒)
        lock.unlock();
    }
}

运行结果:线程1尝试获取锁时,主线程持有锁1.5秒,超过线程1的超时时间(1秒),因此线程1输出“超时未获取到锁”,不会无限阻塞。

关键注意:超时时间的设置需结合业务场景——时间过短可能导致频繁获取锁失败,时间过长则无法有效避免死锁,通常设置为 1~5 秒(根据业务耗时调整)。

2.3 条件变量(Condition)的使用(多线程协作神器)

ReentrantLock 通过 newCondition() 方法创建 Condition 对象,实现线程间的协作通信,相比 synchronized 的 wait()/notify(),Condition 支持“多条件等待”,能精准唤醒指定条件的线程,避免无效唤醒,提升并发效率。

核心对比:synchronized 只有一个“条件队列”,调用 notifyAll() 会唤醒所有等待的线程,即使某些线程不满足唤醒条件,也会被唤醒,造成无效调度;而 Condition 可以为每个条件创建一个独立的队列,调用 signal() 仅唤醒该条件队列中的线程,精准高效。

实战示例:用 Condition 实现生产者-消费者模式(经典场景)

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

public class ConditionDemo {
    // 锁实例
    private final Lock lock = new ReentrantLock();
    // 条件1:队列非空(消费者等待该条件,生产者唤醒)
    private final Condition notEmpty = lock.newCondition();
    // 条件2:队列非满(生产者等待该条件,消费者唤醒)
    private final Condition notFull = lock.newCondition();
    // 共享队列(生产者生产数据,消费者消费数据)
    private final Queue<Integer> queue = new LinkedList<>();
    // 队列容量
    private static final int CAPACITY = 5;

    // 生产者:生产数据,放入队列
    public void put(int value) throws InterruptedException {
        lock.lock();
        try {
            // 队列满时,生产者等待(等待“非满”条件)
            while (queue.size() == CAPACITY) {
                System.out.println("队列已满,生产者等待...");
                notFull.await(); // 释放锁,进入notFull条件队列等待
            }
            // 生产数据,放入队列
            queue.add(value);
            System.out.println("生产:" + value + ",队列大小:" + queue.size());
            // 唤醒等待“非空”条件的消费者(队列有数据了)
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    // 消费者:从队列中消费数据
    public int take() throws InterruptedException {
        lock.lock();
        try {
            // 队列空时,消费者等待(等待“非空”条件)
            while (queue.isEmpty()) {
                System.out.println("队列为空,消费者等待...");
                notEmpty.await(); // 释放锁,进入notEmpty条件队列等待
            }
            // 消费数据,从队列中取出
            int value = queue.poll();
            System.out.println("消费:" + value + ",队列大小:" + queue.size());
            // 唤醒等待“非满”条件的生产者(队列有空闲位置了)
            notFull.signal();
            return value;
        } finally {
            lock.unlock();
        }
    }

    // 测试生产者-消费者模式
    public static void main(String[] args) {
        ConditionDemo demo = new ConditionDemo();

        // 3个生产者线程
        for (int i = 0; i < 3; i++) {
            int producerId = i;
            new Thread(() -> {
                try {
                    for (int j = 0; j < 3; j++) {
                        demo.put(producerId * 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 < 4; j++) {
                        demo.take();
                        Thread.sleep(800); // 模拟消费耗时
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "消费者" + (i+1)).start();
        }
    }
}

运行结果解析:

当队列满时,生产者调用 notFull.await() 进入等待状态,释放锁;当消费者消费数据后,调用 notFull.signal() 唤醒生产者,继续生产;同理,队列空时,消费者等待,生产者生产后唤醒消费者,实现了高效的线程协作。

核心优势:Condition 将“队列满”和“队列空”两个条件分离,避免了 synchronized 中 notifyAll() 唤醒所有线程的无效开销,提升了并发效率,是多线程协作场景的首选方案。

三、ReentrantReadWriteLock:读写分离的锁机制,优化读多写少场景

在实际开发中,很多场景存在“读多写少”的特点(如缓存读取、配置查询、日志查看)——读操作可以并发执行(多个线程同时读,不会导致数据不一致),而写操作需要独占访问(同一时间只能有一个线程写,否则会导致数据错乱)。

ReentrantLock 和 synchronized 都是“独占锁”(同一时间只能有一个线程持有锁),在“读多写少”场景下,会导致读操作相互阻塞,严重影响并发性能。而 ReentrantReadWriteLock(读写锁)通过“分离读锁和写锁”,实现“读共享、写独占”,大幅提升读多写少场景的吞吐量。

3.1 核心特性(底层+关键规则)

ReentrantReadWriteLock 实现了 Lock 接口的子接口 ReadWriteLock,包含两个核心锁:ReadLock(读锁,共享锁)和 WriteLock(写锁,独占锁),其核心特性如下:

  • 读写分离:读锁可被多个线程同时持有(共享),写锁只能被一个线程持有(独占);
  • 可重入性:读锁和写锁都支持可重入——同一线程可多次获取读锁,也可多次获取写锁(需对应次数释放);
  • 锁降级支持:写锁可降级为读锁(流程:先获取写锁 → 再获取读锁 → 最后释放写锁),但读锁不能升级为写锁(避免死锁);
  • 公平性控制:支持公平锁和非公平锁(构造函数传入 true 为公平锁),默认非公平锁。

关键:锁的兼容性规则(面试高频,必须掌握)——判断新请求的锁能否获取,取决于当前持有锁的类型,具体规则如下:

当前持有锁 新请求的锁 能否获取 说明
无锁 读锁 多个线程可同时获取读锁,实现读共享
无锁 写锁 写锁独占,只有一个线程能获取
读锁(多个线程持有) 读锁 读锁共享,新线程可直接获取
读锁(多个线程持有) 写锁 不能 写锁需独占,需等待所有读锁释放
写锁(单个线程持有) 读锁 同一线程可获取读锁,实现锁降级
写锁(单个线程持有) 写锁 同一线程可重入写锁,其他线程不能获取

3.2 基本使用方法(读锁 vs 写锁)

ReentrantReadWriteLock 的使用需分别获取读锁和写锁,遵循“读操作使用读锁,写操作使用写锁”的原则,释放锁同样需放在 finally 块中。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
    // 1. 创建ReentrantReadWriteLock实例(默认非公平锁)
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    // 2. 获取读锁(共享锁)
    private final Lock readLock = rwLock.readLock();
    // 3. 获取写锁(独占锁)
    private final Lock writeLock = rwLock.writeLock();
    // 共享资源:缓存(读多写少场景)
    private Map<String, Object> cache = new HashMap<>();

    // 读操作:查询缓存,使用读锁
    public Object get(String key) {
        readLock.lock(); // 获取读锁
        try {
            System.out.println("线程" + Thread.currentThread().getName() + "读取key:" + key);
            // 模拟读操作耗时
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            return cache.get(key);
        } finally {
            readLock.unlock(); // 释放读锁
        }
    }

    // 写操作:更新缓存,使用写锁
    public void put(String key, Object value) {
        writeLock.lock(); // 获取写锁
        try {
            System.out.println("线程" + Thread.currentThread().getName() + "写入key:" + key + ",value:" + value);
            // 模拟写操作耗时
            try { Thread.sleep(200); } catch (InterruptedException e) {}
            cache.put(key, value);
        } finally {
            writeLock.unlock(); // 释放写锁
        }
    }

    // 测试读多写少场景
    public static void main(String[] args) {
        ReadWriteLockDemo demo = new ReadWriteLockDemo();

        // 5个读线程(并发读)
        for (int i = 0; i < 5; i++) {
            int readId = i;
            new Thread(() -> {
                demo.get("key" + readId);
            }, "读线程" + (readId+1)).start();
        }

        // 2个写线程(独占写)
        for (int i = 0; i < 2; i++) {
            int writeId = i;
            new Thread(() -> {
                demo.put("key" + writeId, "value" + writeId);
            }, "写线程" + (writeId+1)).start();
        }
    }
}

运行结果特点:

  • 5个读线程可以同时执行读操作,无需相互阻塞,提升了读并发效率;
  • 写线程执行写操作时,所有读线程和其他写线程都会阻塞,确保写操作的原子性;
  • 写操作完成后,读线程才能继续执行,保证数据一致性。

性能优势:在高并发读场景下,ReentrantReadWriteLock 的吞吐量远高于 ReentrantLock 和 synchronized——例如,100个读线程并发执行时,ReentrantReadWriteLock 可支持100个线程同时读,而 ReentrantLock 只能让线程串行执行,性能差距显著。

3.3 锁降级示例(实战场景+底层意义)

锁降级是 ReentrantReadWriteLock 的核心特性之一,指“写锁持有者先获取读锁,再释放写锁”,最终持有读锁的过程。锁降级的核心意义是:确保写操作完成后,读操作能立即看到最新数据,且不会被其他写操作中断。

注意:锁降级不能反向(读锁不能升级为写锁)——如果多个线程持有读锁,其中一个线程尝试获取写锁,会导致该线程阻塞,直到所有读锁释放,若此时其他线程继续获取读锁,会导致死锁。

实战示例(锁降级的使用):

public class LockDowngradeDemo {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    private String data = "初始数据";

    // 锁降级流程:写锁 → 读锁 → 释放写锁 → 释放读锁
    public void downgradeLock() {
        // 1. 获取写锁(独占)
        writeLock.lock();
        try {
            System.out.println("获取写锁,开始更新数据");
            // 更新数据(写操作)
            data = "更新后的数据";
            // 2. 获取读锁(同一线程,可在持有写锁时获取读锁)
            readLock.lock();
            System.out.println("获取读锁,完成锁降级");
        } finally {
            // 3. 释放写锁(此时仍持有读锁,其他线程可获取读锁,但不能获取写锁)
            writeLock.unlock();
            System.out.println("释放写锁,保留读锁");
        }

        // 4. 持有读锁,执行读操作
        try {
            System.out.println("持有读锁,读取数据:" + data);
            // 模拟读操作耗时
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 5. 释放读锁,锁完全释放
            readLock.unlock();
            System.out.println("释放读锁,锁完全释放");
        }
    }

    public static void main(String[] args) {
        LockDowngradeDemo demo = new LockDowngradeDemo();
        new Thread(demo::downgradeLock, "锁降级线程").start();
    }
}

运行结果解析:

线程先获取写锁,更新数据后,获取读锁,再释放写锁——此时线程仍持有读锁,可安全读取最新数据,且其他线程可获取读锁(共享),但无法获取写锁(需等待读锁释放),确保了数据一致性和读并发效率。

应用场景:写操作后需要立即读取数据,且希望后续读操作能并发执行(如缓存更新后,立即读取缓存,同时允许其他线程读取)。

四、Lock 与 synchronized 的全面对比及选择指南(实战必备)

在实际开发中,选择 Lock 还是 synchronized,核心取决于业务场景——没有“最优”的锁,只有“最合适”的锁。以下是两者的全面对比,以及具体场景的选择建议。

4.1 功能对比(表格清晰呈现,面试必背)

特性 Lock(以 ReentrantLock 为例) synchronized
可重入性 支持 支持
公平性 可设置公平/非公平锁 仅非公平锁
锁获取方式 显式(lock()/unlock()) 隐式(代码块/方法)
可中断性 支持(lockInterruptibly()) 不支持
超时获取 支持(tryLock(time)) 不支持
条件变量 支持多条件(Condition) 仅单条件(wait()/notify())
性能 高竞争场景下更优,读多写少场景可配合 ReentrantReadWriteLock 优化 低竞争场景下接近 Lock,高竞争场景下略差
灵活性 高(可自定义扩展,支持多种锁策略) 低(固定实现,无扩展能力)
锁泄漏风险 有(忘记 unlock() 会导致锁泄漏) 无(JVM 自动释放锁)

4.2 适用场景选择指南(实战落地)

结合业务场景,给出明确的选择建议,避免盲目使用 Lock 或 synchronized:

优先使用 synchronized 的场景

  • 简单同步场景:如单例模式、简单计数器、少量共享资源修改,语法简洁,不易出错;
  • 无复杂需求场景:无需中断、无需超时、无需多条件通信,仅需基础互斥;
  • 单线程或低并发场景:性能差异可忽略,synchronized 更简洁,且 JVM 可自动优化(锁消除、锁粗化)。

优先使用 ReentrantLock 的场景

  • 需要中断等待锁的线程:如用户取消操作、任务超时,需通过 lockInterruptibly() 中断线程;
  • 需要超时获取锁:避免死锁(如多锁竞争场景),通过 tryLock(time) 设置超时时间;
  • 需要多条件变量通信:如生产者-消费者、任务调度,通过 Condition 实现精准唤醒;
  • 需要公平锁:对线程调度公平性要求高的场景(如资源调度系统)。

优先使用 ReentrantReadWriteLock 的场景

  • 读多写少场景:如缓存读取、配置查询、日志查看,读操作远多于写操作;
  • 需要提升读并发性能:希望多个线程同时执行读操作,避免读操作相互阻塞。

五、实战案例:用 ReentrantLock 解决死锁问题(生产级方案)

死锁是并发编程中的常见问题,其产生的核心条件是“多个线程持有不同的锁,且相互等待对方释放锁”。synchronized 由于不支持超时和中断,一旦发生死锁,只能通过重启应用解决;而 ReentrantLock 的 tryLock() 方法(超时获取),可有效打破死锁的“循环等待”条件,避免死锁。

场景描述

两个线程分别需要获取两把锁(lockA 和 lockB),但获取顺序相反:线程1先获取 lockA,再获取 lockB;线程2先获取 lockB,再获取 lockA。使用 synchronized 会导致死锁,用 ReentrantLock 的 tryLock() 可完美解决。

解决方案(生产级代码,含注释)

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

public class DeadlockSolution {
    // 定义两把锁
    private final Lock lockA = new ReentrantLock();
    private final Lock lockB = new ReentrantLock();

    // 线程1的操作:先获取lockA,再获取lockB
    public void operation1() throws InterruptedException {
        // 1. 超时尝试获取lockA(1秒超时),避免无限等待
        if (lockA.tryLock(1, TimeUnit.SECONDS)) {
            try {
                System.out.println("线程" + Thread.currentThread().getName() + "获取到lockA");
                Thread.sleep(100); // 模拟业务操作,持有lockA一段时间

                // 2. 超时尝试获取lockB(1秒超时)
                if (lockB.tryLock(1, TimeUnit.SECONDS)) {
                    try {
                        System.out.println("线程" + Thread.currentThread().getName() + "获取到lockB,执行核心操作");
                        // 核心业务逻辑
                    } finally {
                        // 3. 释放lockB
                        lockB.unlock();
                        System.out.println("线程" + Thread.currentThread().getName() + "释放lockB");
                    }
                } else {
                    // 4. 获取lockB超时,释放已获取的lockA,打破循环等待
                    System.out.println("线程" + Thread.currentThread().getName() + "获取lockB超时,释放lockA");
                }
            } finally {
                // 5. 释放lockA(无论是否获取到lockB,都要释放lockA)
                lockA.unlock();
                System.out.println("线程" + Thread.currentThread().getName() + "释放lockA");
            }
        } else {
            // 6. 获取lockA超时,放弃操作
            System.out.println("线程" + Thread.currentThread().getName() + "获取lockA超时,放弃操作");
        }
    }

    // 线程2的操作:先获取lockB,再获取lockA
    public void operation2() throws InterruptedException {
        if (lockB.tryLock(1, TimeUnit.SECONDS)) {
            try {
                System.out.println("线程" + Thread.currentThread().getName() + "获取到lockB");
                Thread.sleep(100);

                if (lockA.tryLock(1, TimeUnit.SECONDS)) {
                    try {
                        System.out.println("线程" + Thread.currentThread().getName() + "获取到lockA,执行核心操作");
                    } finally {
                        lockA.unlock();
                        System.out.println("线程" + Thread.currentThread().getName() + "释放lockA");
                    }
                } else {
                    System.out.println("线程" + Thread.currentThread().getName() + "获取lockA超时,释放lockB");
                }
            } finally {
                lockB.unlock();
                System.out.println("线程" + Thread.currentThread().getName() + "释放lockB");
            }
        } else {
            System.out.println("线程" + Thread.currentThread().getName
Logo

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

更多推荐