首页 / Java 入门教程 / 显式锁 Lock 与 ReentrantLock

Java 入门教程

显式锁 Lock 与 ReentrantLock

本教程共 100 篇 · 第 99 篇 · 更新于 2026-08-05 · 约 15 分钟阅读

JavaJava 入门教程ReentrantLockLocktryLock读写锁

99. 显式锁 Lock 与 ReentrantLock

本节目标:掌握 ReentrantLock 的标准用法,理解它相比 synchronized 多出来的那些能力。

synchronized 差在哪

synchronized 简单可靠,但有几个绕不过去的限制。

拿不到锁只能死等。一旦进入 BLOCKED,就没有任何退路,不能设超时,也不能放弃。

不能被中断。等锁的线程调 interrupt() 没有任何反应,只能一直等下去。

无法知道是否成功。没有「试着拿一下,拿不到就先做别的」这种选项。

加锁范围受语法约束。必须在同一个代码块里加锁和解锁,无法在方法 A 加锁、方法 B 解锁。

Java 5 引入的 java.util.concurrent.locks 包,用显式锁补齐了这些能力。

Lock 接口

Lock 是一个接口,定义了锁的基本行为:

方法说明
lock()获取锁,拿不到就一直等
tryLock()尝试获取,立即返回 truefalse
tryLock(time, unit)限时尝试,超时返回 false
lockInterruptibly()获取锁,等待期间可被中断
unlock()释放锁

最常用的实现类是 ReentrantLock,名字里的 Reentrant 就是「可重入」,这一点和 synchronized 一致。

标准写法

显式锁最大的特点是必须手动解锁。忘了解锁,其他线程就永远进不来。

所以有一套雷打不动的写法:

import java.util.concurrent.locks.*;

public class LockCounter {
    private static int count = 0;
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 100_000; i++) {
                lock.lock();                 // 加锁写在 try 之前
                try {
                    count++;
                } finally {
                    lock.unlock();           // 解锁必须在 finally 里
                }
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("结果:" + count);
    }
}
```text

输出:

```text
结果:200000

两个细节值得强调。

lock() 要写在 try 外面。如果写在里面,万一加锁本身失败抛了异常,finally 里的 unlock() 会对一把没拿到的锁解锁,抛出 IllegalMonitorStateException,掩盖真正的错误。

unlock() 必须写在 finally 里。临界区代码一旦抛异常,没有 finally 就跳过解锁了,这把锁永远释放不掉,所有等待的线程全部卡死。

Warning

这是显式锁最大的风险点。synchronized 由 JVM 保证退出代码块时自动释放锁,哪怕抛异常也一样。用 Lock 就得自己扛这个责任,try-finally 一个都不能少。

优势一:tryLock 不死等

tryLock() 尝试获取锁,成功返回 true,失败立刻返回 false,绝不阻塞。

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

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

    public static void main(String[] args) {
        Runnable task = () -> {
            String name = Thread.currentThread().getName();
            try {
                // 最多等 500 毫秒
                if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
                    try {
                        System.out.println(name + " 拿到锁,开始工作");
                        Thread.sleep(1000);
                    } finally {
                        lock.unlock();
                        System.out.println(name + " 释放锁");
                    }
                } else {
                    System.out.println(name + " 等待超时,改做别的事");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        new Thread(task, "线程1").start();
        new Thread(task, "线程2").start();
    }
}
```text

输出:

```text
线程1 拿到锁,开始工作
线程2 等待超时,改做别的事
线程1 释放锁

线程2 没有傻等,超时后走了另一条路径。

这个能力在实际开发中很有用。用户点了按钮,后台任务正忙,与其让界面卡住,不如立刻提示「系统繁忙,请稍后重试」。

tryLock 还是防死锁的利器。上一节转账的例子,改成「两把锁都 tryLock 成功才继续,否则全部释放后重试」,就能从根本上避免死锁。

优势二:可中断

lockInterruptibly() 在等待锁的过程中响应中断:

import java.util.concurrent.locks.*;

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

    public static void main(String[] args) throws InterruptedException {
        Thread holder = new Thread(() -> {
            lock.lock();
            try {
                Thread.sleep(5000);   // 长时间占着锁
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }, "持锁线程");

        Thread waiter = new Thread(() -> {
            try {
                lock.lockInterruptibly();   // 可被打断的等待
                try {
                    System.out.println("拿到锁");
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                System.out.println("等锁时被中断,放弃等待");
            }
        }, "等锁线程");

        holder.start();
        Thread.sleep(100);
        waiter.start();
        Thread.sleep(500);
        waiter.interrupt();     // 打断等待
    }
}
```text

输出:

```text
等锁时被中断,放弃等待

换成 synchronizedwaiter 会一直等满 5 秒,interrupt() 完全无效。

优势三:公平锁

ReentrantLock 构造时可以传一个布尔值,选择公平模式:

Lock fair = new ReentrantLock(true);    // 公平锁
Lock unfair = new ReentrantLock();      // 非公平锁(默认)
```bash

**非公平锁**:锁释放时,所有等待线程一起抢,谁抢到算谁的。新来的线程可能插队,排了很久的线程反而抢不到。

**公平锁**:严格按申请顺序排队,先到先得,不会有线程被饿死。

听起来公平锁更好,但默认是非公平的,原因是**性能**

非公平锁允许刚释放锁的线程立刻再次获得它,省掉了唤醒队列中其他线程的开销。线程唤醒和上下文切换的成本很高,减少这个动作带来的吞吐提升明显。

> [!TIP]
> 除非确实观察到某些线程长期抢不到锁,否则用默认的非公平锁。公平锁的吞吐量通常明显更低。

## Condition:精准唤醒

`synchronized` 配套的是 `wait()`/`notify()`,只有一个等待队列。调 `notifyAll()` 会把所有等待线程都叫醒,让它们重新竞争,很浪费。

`Lock` 配套的是 `Condition`,一把锁可以创建多个条件队列,实现精准唤醒:

```java
import java.util.concurrent.locks.*;

public class ConditionDemo {
    private static final Lock lock = new ReentrantLock();
    private static final Condition notEmpty = lock.newCondition();
    private static int data = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread consumer = new Thread(() -> {
            lock.lock();
            try {
                while (data == 0) {
                    notEmpty.await();       // 等待并释放锁
                }
                System.out.println("消费到数据:" + data);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        });

        consumer.start();
        Thread.sleep(300);

        lock.lock();
        try {
            data = 42;
            notEmpty.signal();              // 唤醒等待该条件的线程
        } finally {
            lock.unlock();
        }
        consumer.join();
    }
}

输出:

消费到数据:42
```bash

`await()` 对应 `wait()`,`signal()` 对应 `notify()`,`signalAll()` 对应 `notifyAll()`。

生产者消费者模型中,可以建两个 `Condition`:`notFull` 让生产者等,`notEmpty` 让消费者等。队列满时只唤醒消费者,空时只唤醒生产者,互不打扰。

注意 `await()` 必须写在 `while` 循环里检查条件,不能用 `if`。线程被唤醒后条件未必真的满足(可能被别的线程抢先消费了),必须重新检查。

## 读写锁

`ReentrantReadWriteLock` 是另一个实用工具,它把锁拆成读锁和写锁:

- 多个线程可以**同时持有读锁**
- 写锁是**独占**的,与读锁和其他写锁都互斥

```java
import java.util.concurrent.locks.*;
import java.util.*;

public class CacheDemo {
    private final Map<String, String> cache = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public String get(String key) {
        rwLock.readLock().lock();      // 读锁:多线程可并发
        try {
            return cache.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void put(String key, String value) {
        rwLock.writeLock().lock();     // 写锁:独占
        try {
            cache.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

读多写少的场景(配置中心、本地缓存、字典表)用它收益很大。所有读操作并发执行,只有写的时候才互斥。

反过来,写操作频繁时读写锁反而更慢——维护两套状态本身有开销,还不如直接用 ReentrantLock

该用哪个

场景选择
简单的互斥保护synchronized
需要超时或尝试获取ReentrantLock + tryLock
等锁期间需要能中断ReentrantLock + lockInterruptibly
需要多个等待队列ReentrantLock + Condition
读多写少ReentrantReadWriteLock

日常开发优先用 synchronized。它写法简洁,不会忘记解锁,而且现代 JVM 对它做了大量优化(偏向锁、轻量级锁、锁消除),性能和 ReentrantLock 基本持平。

只有确实需要那些额外能力时,才换成显式锁。

本节小结

  • Lock 是接口,ReentrantLock 是最常用的实现,同样可重入。
  • 标准写法:lock() 在 try 外,unlock() 在 finally 内,一个都不能少。
  • tryLock() 不阻塞,带超时版本可以等一会儿再放弃。
  • lockInterruptibly() 让等锁的线程能响应中断。
  • 默认非公平锁性能更好,公平锁只在防饥饿时使用。
  • Condition 支持多个等待队列,实现精准唤醒。
  • await() 必须放在 while 循环里重新检查条件。
  • 读写锁适合读多写少,写频繁时反而更慢。
  • 没有特殊需求就用 synchronized,更安全也更简洁。