源本科技 | 码上会

Java 中的锁机制

2026/01/29
44
0

学习目标

  • 理解 Java 中锁(Lock)的基本概念及其在线程同步中的作用

  • 掌握 ReentrantLockReadWriteLock 的使用方法与适用场景

  • 熟悉 Lock 接口提供的核心方法及其行为差异

  • 能够在实际多线程程序中正确应用锁机制保障线程安全


什么是锁

在 Java 并发编程中,是一种用于控制多个线程对共享资源访问的同步机制。它确保在任意时刻,只有一个(或特定规则下的一组)线程可以执行关键代码段(临界区),从而避免数据竞争和状态不一致。与传统的 synchronized 关键字相比,java.util.concurrent.locks.Lock 接口提供了更灵活、更强大的锁操作能力,例如可中断的锁获取、超时尝试、公平性策略等。

基本使用模式如下:

Lock lock = new ReentrantLock();
lock.lock();   // 获取锁
try {
    // 临界区:访问共享资源
} finally {
    lock.unlock(); // 释放锁(必须放在 finally 块中)
}

注意:unlock() 必须在 finally 块中调用,以确保即使发生异常也能释放锁,避免死锁。


锁的类型

1. 可重入锁

ReentrantLockLock 接口的一个实现类,支持可重入性——即同一个线程可以多次获取同一把锁而不会导致死锁。

示例:使用 ReentrantLock 控制线程顺序执行

import java.util.concurrent.locks.ReentrantLock;

/**
 * 模拟一个需要加锁执行的任务
 */
class ChineseTask implements Runnable {
    private final ReentrantLock lock;
    private final String threadName;

    public ChineseTask(ReentrantLock lock, String name) {
        this.lock = lock;
        this.threadName = name;
    }

    @Override
    public void run() {
        lock.lock(); // 获取锁
        try {
            System.out.println(threadName + " 成功获取锁");
            Thread.sleep(1000); // 模拟耗时操作
            System.out.println(threadName + " 完成任务");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢复中断状态
        } finally {
            lock.unlock(); // 确保在 finally 块中释放锁
        }
    }
}

/**
 * 可重入锁(ReentrantLock)演示:两个线程竞争同一把锁
 */
public class LockDemo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();

        Thread thread1 = new Thread(new ChineseTask(lock, "线程-1"));
        Thread thread2 = new Thread(new ChineseTask(lock, "线程-2"));

        thread1.start();
        thread2.start();
    }
}

输出示例:

线程-1 成功获取锁
线程-1 完成任务
线程-2 成功获取锁
线程-2 完成任务

说明:
尽管两个线程几乎同时启动,但只有第一个成功获取锁的线程(如 Thread-1)能进入临界区。Thread-2 会阻塞等待,直到 Thread-1 调用 unlock() 释放锁后才能继续执行。


2. 读写锁

ReadWriteLock 接口提供了一种更细粒度的并发控制机制:允许多个读线程同时访问共享资源,但写操作是独占的——即写时不能读,读时不能写。Java 提供了 ReentrantReadWriteLock 作为其实现。

示例:使用 ReadWriteLock 实现线程安全的数据读写

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 共享数据容器,使用读写锁保护读写操作
 */
class SharedList {
    private final List<String> data = new ArrayList<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();   // 读锁:允许多个线程并发读
    private final Lock writeLock = rwLock.writeLock(); // 写锁:独占,禁止其他读/写

    /**
     * 写操作:向列表中添加元素
     */
    public void add(String item) {
        writeLock.lock();
        try {
            data.add(item);
            System.out.println(Thread.currentThread().getName() + " 添加了: " + item);
        } finally {
            writeLock.unlock(); // 确保写锁被释放
        }
    }

    /**
     * 读操作:读取指定索引处的元素
     */
    public void read(int index) {
        readLock.lock();
        try {
            if (index < data.size()) {
                System.out.println(Thread.currentThread().getName() + " 读取到: " + data.get(index));
            } else {
                System.out.println(Thread.currentThread().getName() + " 尝试读取越界索引: " + index);
            }
        } finally {
            readLock.unlock(); // 确保读锁被释放
        }
    }
}

/**
 * 演示读写锁(ReentrantReadWriteLock)的使用:
 * - 写操作互斥(同一时间只有一个写线程)
 * - 读操作可并发(多个读线程可同时执行)
 * - 写优先于读(默认策略下,写请求会阻塞后续读请求)
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        SharedList shared = new SharedList();

        // 创建两个写线程
        Thread writer1 = new Thread(() -> shared.add("你好"), "写线程-1");
        Thread writer2 = new Thread(() -> shared.add("世界"), "写线程-2");

        // 创建两个读线程
        Thread reader1 = new Thread(() -> shared.read(0), "读线程-1");
        Thread reader2 = new Thread(() -> shared.read(1), "读线程-2");

        // 启动写线程
        writer1.start();
        writer2.start();

        // 等待所有写操作完成后再启动读线程(简化逻辑,避免读取空数据)
        try {
            writer1.join();
            writer2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 启动读线程
        reader1.start();
        reader2.start();
    }
}

可能的输出:

写线程-1 添加了: 你好
写线程-2 添加了: 世界
读线程-1 读取到: 你好
读线程-2 读取到: 世界

说明:

  • 写锁(writeLock)是独占的:同一时间只能有一个写线程持有。

  • 读锁(readLock)是共享的:多个读线程可同时持有,互不阻塞。

  • 读写互斥:只要有线程在读,写线程必须等待;反之亦然。

这种机制特别适合“读多写少”的场景(如缓存、配置管理),能显著提升并发性能。


核心方法

Lock 接口提供了比 synchronized 更丰富的控制能力,主要方法如下:

方法

描述

void lock()

获取锁,若已被其他线程持有,则当前线程阻塞等待

void lockInterruptibly()

获取锁,但允许在等待过程中响应线程中断(抛出 InterruptedException

boolean tryLock()

非阻塞尝试获取锁,立即返回 true(成功)或 false(失败)

boolean tryLock(long time, TimeUnit unit)

在指定时间内尝试获取锁,超时则返回 false

void unlock()

释放当前线程持有的锁

Condition newCondition()

创建与该锁关联的 Condition 对象,用于实现类似 wait/notify 的线程协作

使用 tryLock 避免死锁

Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();

// 线程尝试按固定顺序获取锁,若无法获取则放弃
if (lock1.tryLock()) {
    try {
        if (lock2.tryLock()) {
            try {
                // 执行需要两个锁的操作
            } finally {
                lock2.unlock();
            }
        }
    } finally {
        lock1.unlock();
    }
}
// 若任一锁获取失败,可执行回退逻辑

锁机制对比

特性

synchronized

Lock(如 ReentrantLock)

语法

关键字,自动加锁 / 释放

API 调用,需手动管理

可中断

不支持

支持(lockInterruptibly()

超时获取

不支持

支持(tryLock(timeout)

公平性

非公平(JVM 优化)

可构造为公平锁(new ReentrantLock(true)

条件变量

wait/notify

支持多个 Condition 对象

性能

JVM 高度优化,低竞争下更快

高竞争下更灵活,但开销略大

建议:简单同步场景优先使用 synchronized;需要高级控制(如超时、中断、多条件)时选用 Lock


重点总结

  • 锁的核心作用是保证多线程环境下对共享资源的安全访问。

  • ReentrantLock 提供了比 synchronized 更灵活的锁控制,支持可重入、可中断、超时等特性。

  • ReadWriteLock 通过分离读写权限,在“读多写少”场景下显著提升并发性能。

  • 使用 Lock 时务必在 finally 块中释放锁,防止死锁。

  • 根据实际需求选择合适的同步机制:简单场景用 synchronized,复杂并发控制用 Lock


思考题

  1. 为什么 ReentrantLock 被称为“可重入”?请举例说明一个线程如何多次获取同一把锁而不死锁。

  2. 在高并发读多写少的系统中(如商品详情页缓存),为什么 ReadWriteLocksynchronizedReentrantLock 更合适?

  3. 如果你在 lock.lock()lock.unlock() 之间忘记使用 try-finally,可能会导致什么严重后果?如何通过工具或编码规范避免此类问题?