理解 Java 中锁(Lock)的基本概念及其在线程同步中的作用
掌握 ReentrantLock 和 ReadWriteLock 的使用方法与适用场景
熟悉 Lock 接口提供的核心方法及其行为差异
能够在实际多线程程序中正确应用锁机制保障线程安全
在 Java 并发编程中,锁是一种用于控制多个线程对共享资源访问的同步机制。它确保在任意时刻,只有一个(或特定规则下的一组)线程可以执行关键代码段(临界区),从而避免数据竞争和状态不一致。与传统的 synchronized 关键字相比,java.util.concurrent.locks.Lock 接口提供了更灵活、更强大的锁操作能力,例如可中断的锁获取、超时尝试、公平性策略等。
基本使用模式如下:
Lock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 临界区:访问共享资源
} finally {
lock.unlock(); // 释放锁(必须放在 finally 块中)
}注意:
unlock()必须在finally块中调用,以确保即使发生异常也能释放锁,避免死锁。
ReentrantLock 是 Lock 接口的一个实现类,支持可重入性——即同一个线程可以多次获取同一把锁而不会导致死锁。
示例:使用 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() 释放锁后才能继续执行。
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 更丰富的控制能力,主要方法如下:
使用 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 提供了比 synchronized 更灵活的锁控制,支持可重入、可中断、超时等特性。
ReadWriteLock 通过分离读写权限,在“读多写少”场景下显著提升并发性能。
使用 Lock 时务必在 finally 块中释放锁,防止死锁。
根据实际需求选择合适的同步机制:简单场景用 synchronized,复杂并发控制用 Lock。
为什么 ReentrantLock 被称为“可重入”?请举例说明一个线程如何多次获取同一把锁而不死锁。
在高并发读多写少的系统中(如商品详情页缓存),为什么 ReadWriteLock 比 synchronized 或 ReentrantLock 更合适?
如果你在 lock.lock() 和 lock.unlock() 之间忘记使用 try-finally,可能会导致什么严重后果?如何通过工具或编码规范避免此类问题?