Java锁:锁的基本概念
在 Java 并发编程中,"锁" 是解决多线程安全问题最核心的工具。常见的锁包括:synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock、Semaphore、CountDownLatch / CyclicBarrier 等。
本文从不同维度对 Java 锁进行归类梳理,帮助你建立一套清晰的锁知识体系。
一、按实现方式分类
Java 层面的锁按实现方式可划分为以下三大类:
内置语言锁(唯一关键字)
synchronized:Java 语言层面唯一的内置锁,由 JVM 底层(C++)实现。使用最简洁,无需手动释放锁(JVM 自动加锁/解锁)。
JUC 显式锁(java.util.concurrent.locks 包)
ReentrantLock 是该包下最常见的实现,但绝非唯一,包内还包括:
ReentrantReadWriteLock(读写锁):维护一对锁——读锁和写锁。读锁可被多个读线程共享(共享锁),写锁是独占的,适合"读多写少"场景,并发度高于ReentrantLock。StampedLock(JDK 1.8 引入):读写锁的改进版,支持乐观读(tryOptimisticRead())。乐观读期间不阻塞写线程,极大提高了读多写少场景的吞吐量(注意:它不可重入)。
广义的"同步工具"(虽不叫 Lock,但控制并发)
在面试和源码中,这些类底层同样依赖 AQS(AbstractQueuedSynchronizer,抽象队列同步器),本质上也是 Java 层面解决同步问题的手段,常被归为"锁的变种":
Semaphore(信号量):控制同时访问某资源的线程数量(许可数),可理解为一种"共享锁"。CountDownLatch/CyclicBarrier:栅栏/计数器,用于线程间协调等待。虽不直接互斥临界区,但属于同步机制的一部分。
除了上述按"实现方式"的划分,我们还常听到悲观锁、乐观锁、公平锁、非公平锁、共享锁、排他锁等说法——这些是从不同设计维度对锁进行的性质归类。下面逐一解释。
二、按对共享资源的访问方式分类
悲观锁与乐观锁,是按照对共享资源的访问策略进行区分的。
悲观锁(Pessimistic Lock)
悲观锁是一种并发控制策略,它的核心假设是:并发环境下,多个线程对共享资源的访问极大概率会发生冲突。因此在访问资源之前,线程必须先获取并锁定资源,直到该线程完成访问并释放锁,其他线程才能继续。
悲观锁的作用是在多线程环境中防止数据被并发修改,确保数据的一致性和完整性。当一个线程获取了悲观锁后,其他线程必须等待锁释放,从而避免了数据竞态条件和脏读等问题。
适用场景:写操作较多、读操作较少的并发场景。
实现方式:
- 通过
synchronized关键字实现的对象锁或类锁 - 通过
java.util.concurrent.locks.Lock接口的实现类,如ReentrantLock
缺点:
- 性能开销:频繁的加锁和解锁操作可能带来较大性能消耗,高并发场景下可能导致线程频繁上下文切换。
- 可能导致死锁:如果多个线程间的锁获取顺序不当,容易造成死锁。
- 资源利用率低:读多写少场景下,悲观锁可能导致大量读取操作等待,降低系统并发能力和响应速度。
乐观锁(Optimistic Lock)
乐观锁并不是 Java 内置的锁机制,而是一种并发控制策略。它的核心假设是:并发访问环境下,数据竞争不太可能发生,因此读取数据时不会立即加锁,而是在提交更新时才检测是否有冲突。
适用场景:读多写少或并发冲突较少的场景。
实现方式:
- Java 中通过 CAS(Compare and Swap / Compare and Set) 算法实现
- 数据库层面常使用版本号或时间戳等机制进行控制
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger counter = new AtomicInteger(0);
// 乐观锁更新示例
public void incrementCounter() {
while (true) {
int expected = counter.get();
int updated = expected + 1;
if (counter.compareAndSet(expected, updated)) {
// 更新成功,退出循环
break;
}
// 更新失败,意味着有其他线程在此期间改变了值,继续尝试
}
}三、按锁的占有权是否可重入分类
按照一个线程是否可以重复获取同一把锁,可分为:可重入锁与不可重入锁。
可重入锁(Reentrant Lock)
可重入锁具备独特的重入特性:当线程已经获取了锁后,它可以再次请求并成功获得同一把锁,从而避免了在递归调用或嵌套同步块中产生的死锁风险。也就是说,在执行锁保护的代码区域时,即便调用了其他同样被该锁保护的方法或代码片段,持有锁的线程也能顺利完成操作。
在多线程环境下,可重入锁严格限制同一时间只能有一个线程访问特定临界区,有效防止了并发访问引发的数据不一致和竞态条件。此外,通过允许线程在持有锁的状态下重新获取该锁,可重入锁巧妙地解决了同类锁之间由于互相等待而形成的潜在死锁状况,提升了多线程同步的安全性和可靠性。
实现方式:
synchronized关键字:其实现的隐式锁本身就是一种可重入锁。ReentrantLock:java.util.concurrent.locks.ReentrantLock实现了Lock接口,提供显式的可重入锁功能,允许更细粒度的控制(支持公平/非公平、可中断、限时获取等)。ReentrantReadWriteLock:一种特殊的可重入锁,通过读写锁的设计,既保证了线程安全,又能高效处理读多写少的并发场景。
优点:
- 线程安全性:确保多线程环境下的数据一致性。
- 可重入性:简化了代码编写,特别是在递归调用或嵌套同步块的场景中。
- 灵活性:显式可重入锁(如
ReentrantLock)提供了更多控制选项,如尝试获取锁、设置公平性、中断等待线程等。
缺点:
- 使用复杂性:相比于隐式锁(
synchronized),显式锁需要手动管理锁的获取和释放,增加了编程复杂性和出错概率。 - 性能开销:在某些情况下,显式锁可能因为额外的 API 调用和状态管理而带来一定的性能开销。
- 死锁风险:如果开发者不谨慎管理锁的获取和释放顺序,或者滥用锁的特性,仍可能导致死锁。尤其对于显式锁,如果未正确释放
finally中的锁,可能导致资源无法回收。
不可重入锁(Non-reentrant Lock)
不可重入锁的核心特征是:禁止同一个线程在已经持有锁的前提下再度获取相同的锁。若一个线程已取得不可重入锁,在其执行路径中遇到需要再次获取该锁的场景时,该线程将被迫等待,直至原先获取的锁被释放。
此类锁机制同样服务于多线程环境下的资源共享保护,确保同一时间内仅有单一线程能够访问临界资源,从而规避数据不一致和竞态条件。相比于可重入锁,不可重入锁在递归调用或涉及锁嵌套的复杂同步场景下表现出明显的局限性——可能导致线程阻塞和潜在死锁,降低了线程同步的灵活性和安全性。
注意:在 Java 标准库中并没有直接提供名为"不可重入锁"的内置锁,通常通过与
ReentrantLock(可重入锁)对比来理解这一概念。理论上,任何不具备可重入特性的锁都可以认为是不可重入锁;但在实际应用中,Java 的synchronized关键字在目前所有版本中实现的都是可重入锁。
优点:
- 实现简单:从实现角度来看,不可重入锁设计和实现相对简单,因为它不需要处理递归锁定的复杂性。
缺点:
- 容易引发死锁:如果一个线程已持有不可重入锁,又试图再次获取同一把锁,就会形成死锁——线程自身无法推进,也无法释放已持有的锁,其他线程同样无法获取。
- 限制性较强:极大限制了线程的自由度,特别是在递归调用或含有嵌套锁的复杂同步结构中,往往无法满足需求。
- 编程成本高:开发者需要更加小心地管理锁的层次结构,以防无意间陷入死锁或资源浪费。
四、按锁的获取公平性分类
按照获取锁时是否遵循请求顺序(先到先得),可分为:公平锁与非公平锁。
公平锁(Fair Lock)
公平锁遵循"先请求先服务"(First-Come, First-Served, FCFS)原则:按照线程请求锁的先后顺序分配锁资源,等待时间最长的线程优先获得锁。公平锁可以有效避免某个线程长期得不到锁而导致的"饥饿"现象,所有线程都有平等获取锁的机会,调度更加有序。
实现方式:通过 java.util.concurrent.locks.ReentrantLock 的构造函数传入 true,即可创建一个公平的 ReentrantLock 实例。
ReentrantLock fairLock = new ReentrantLock(true); // 创建一个公平锁优点:
- 公平性:所有线程遵循先来后到原则,不会出现新来的线程总是抢占锁的现象,提高了系统的公平性和稳定性。
- 避免线程饥饿:减少或消除了由于锁的不公平分配而导致的线程长时间等待锁的情况。
缺点:
- 性能开销:公平锁在每次释放锁后,都需要检查是否有等待时间更长的线程,通常涉及线程调度的额外开销,可能降低系统整体并发性能。
- 线程上下文切换频繁:为了实现公平性,可能需要频繁进行线程上下文切换,而这本身就是一种相对昂贵的操作。
- 可能产生"车队效应"(Convoy Effect):大量线程因等待前面线程释放锁而形成队列,即使后来的线程只需要很短时间处理,也不得不等待整个队列中的线程依次完成,从而降低系统吞吐量。
非公平锁(Nonfair Lock)
非公平锁不遵循"先请求先服务"原则:锁被释放时,任何等待锁的线程(包括新到达的线程)都可以尝试获取,即使有其他线程已经在等待队列中等待了更长时间。在某些场景下,非公平锁可以提高系统并发性能,因为它允许刚释放锁的线程或其他新到达的线程立刻获取锁,而不是强制排队等待。
实现方式:同样通过 java.util.concurrent.locks.ReentrantLock 的构造函数,传入 false 即可创建一个非公平的 ReentrantLock 实例。ReentrantLock 的默认构造函数也是非公平锁。
ReentrantLock unfairLock = new ReentrantLock(false); // 创建一个非公平锁
// ReentrantLock unfairLock = new ReentrantLock(); // 等价于上面这行,默认非公平优点:
- 性能更优:非公平锁在某些条件下可能提供更高的系统吞吐量,因为它允许线程更快地获取锁,减少线程上下文切换次数。在锁竞争不激烈的场景下,这种效果尤为明显。
缺点:
- 线程饥饿风险:非公平锁可能导致某些线程长时间无法获取锁,因为新到达的线程可能连续多次抢占锁,而早前就在等待的线程始终得不到执行机会。
- 调度不可预测:线程调度的不确定性增大,不利于系统的稳定性和性能分析。
- 潜在连锁反应:线程之间的依赖关系变得复杂,可能引发连锁反应,影响整体系统的性能和稳定性。
五、不同锁的用法示例
5.1 synchronized(内置锁)
适用场景:
- 简单的临界区互斥需求,不想手动管理锁的获取/释放
- 锁竞争不激烈、临界区执行时间较短的场景
- 对代码简洁度要求高,需要快速开发
特点:JVM 自动加锁/解锁,无需手动
unlock(),但不支持公平锁、可中断获取、限时获取等高级功能。
/**
* @className SynchronizedDemo
* @description:
* @author legend
* @version 1.0
*/
public class SynchronizedDemo {
private int count = 0;
// 锁对象:当前实例 (this)
public synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " 执行,count = " + count);
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo demo = new SynchronizedDemo();
Runnable task = () -> { for (int i = 0; i < 5; i++) demo.increment(); };
new Thread(task, "线程A").start();
new Thread(task, "线程B").start();
// 输出:两个线程交替执行,但 count 最终一定累加到 10(原子性保证)
}
}5.2 ReentrantLock(可重入锁)
适用场景:
- 需要公平锁(先来先得),避免线程饥饿
- 需要可中断获取锁(
lockInterruptibly())或限时获取(tryLock(timeout)) - 需要多个
Condition条件队列实现精确的线程唤醒 - 对锁有更细粒度的控制需求
特点:功能比
synchronized更强大,但必须在finally中手动unlock(),否则可能死锁。
public class ReentrantLockDemo {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock(); // 默认非公平
public void increment() {
lock.lock(); // 手动加锁
try {
count++;
System.out.println(Thread.currentThread().getName() + " 执行,count = " + count);
} finally {
lock.unlock(); // 必须在 finally 中释放,否则死锁
}
}
public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
Runnable task = () -> { for (int i = 0; i < 5; i++) demo.increment(); };
new Thread(task, "线程A").start();
new Thread(task, "线程B").start();
}
}5.3 ReentrantReadWriteLock(读写锁)
适用场景:
- 读多写少的场景(如缓存、配置管理)
- 多个线程同时读取不会引发数据冲突,写入时才需要独占
- 需要在保证线程安全的前提下,尽可能提升读的并发度
特点:维护一对「读锁 + 写锁」,读锁共享(多个读线程可同时持有),写锁独占。写线程会阻塞所有读线程和其他写线程。
/**
* @className ReadWriteLockDemo
* @description:
* @author legend
* @version 1.0
*/
public class ReadWriteLockDemo {
private int count = 0;
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作:允许多个线程同时进入
public void read() {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 读取 count = " + count);
Thread.sleep(100); // 模拟读耗时
} catch (InterruptedException e) { e.printStackTrace(); }
finally { rwLock.readLock().unlock(); }
}
// 写操作:独占锁
public void write(int delta) {
rwLock.writeLock().lock();
try {
count += delta;
System.out.println(Thread.currentThread().getName() + " 写入 count = " + count);
} finally {
rwLock.writeLock().unlock();
}
}
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
// 3个读线程:会几乎同时打印(并发执行)
for (int i = 0; i < 3; i++){
new Thread(demo::read, "读线程" + i).start();
}
// 1个写线程:会独占执行,读取必须等待它写完
new Thread(() -> demo.write(10), "写线程").start();
}
}5.4 StampedLock(邮戳锁 / JDK 1.8)
适用场景:
- 读多写少且对吞吐量要求极高的场景(比读写锁更高效)
- 能接受「乐观读」偶尔失败需要升级的逻辑
- 不需要可重入的场景(StampedLock 不可重入,递归调用可能导致死锁)
特点:
ReentrantReadWriteLock的改进版。支持乐观读(tryOptimisticRead()),读期间不阻塞写线程,仅在冲突时才升级为悲观读锁,进一步提升了读多写少场景的吞吐量。
/**
* @className StampedLockDemo
* @description:
* @author legend
* @version 1.0
*/
public class StampedLockDemo {
private int count = 0;
private final StampedLock lock = new StampedLock();
// 写操作(排他)
public void write(int delta) {
long stamp = lock.writeLock(); // 获取写邮戳
try {
count += delta;
System.out.println(Thread.currentThread().getName() + " 写入 count = " + count);
} finally {
lock.unlockWrite(stamp);
}
}
// 读操作(乐观读 + 悲观读兜底)
public void read() {
long stamp = lock.tryOptimisticRead(); // 获取乐观读邮戳
int currentCount = count; // 拷贝变量到线程栈
// 验证:在读取期间是否有写操作发生?
if (!lock.validate(stamp)) {
// 验证失败(被写线程干扰了),升级为悲观读锁
stamp = lock.readLock();
try {
currentCount = count;
System.out.println(Thread.currentThread().getName() + " 升级为悲观读,count = " + currentCount);
} finally {
lock.unlockRead(stamp);
}
} else {
System.out.println(Thread.currentThread().getName() + " 乐观读成功,count = " + currentCount);
}
}
public static void main(String[] args) {
StampedLockDemo demo = new StampedLockDemo();
new Thread(demo::read, "读线程1").start();
new Thread(() -> demo.write(5), "写线程").start();
new Thread(demo::read, "读线程2").start();
// 运行后观察:读线程1 可能乐观读失败,从而升级为悲观读
}
}5.5 Semaphore(信号量 / 共享锁)
适用场景:
- 控制同时访问某资源的最大并发线程数(如数据库连接池、限流)
- 本质是「共享锁」的通用实现——许可数设为 1 时等价于互斥锁
- 生产者-消费者模型中的资源调度
特点:初始化时设置「许可数(permits)」,线程执行前
acquire()获取许可,完成后release()归还。许可数 > 1 时允许多线程并发执行。
/**
* @className SemaphoreDemo
* @description:
* @author legend
* @version 1.0
*/
public class SemaphoreDemo {
// 最多允许 2 个线程同时执行
private static final Semaphore semaphore = new Semaphore(2);
public static void task() {
try {
semaphore.acquire(); // 获取许可(阻塞)
System.out.println(Thread.currentThread().getName() + " 获取许可,正在执行...");
Thread.sleep(2000);
} catch (InterruptedException e) { e.printStackTrace(); }
finally {
semaphore.release(); // 释放许可
System.out.println(Thread.currentThread().getName() + " 释放许可");
}
}
public static void main(String[] args) {
// 启动 5 个线程,但同一时刻最多只有 2 个在执行
for (int i = 0; i < 5; i++) {
new Thread(SemaphoreDemo::task, "线程" + i).start();
}
// 观察控制台:每次只有 2 个线程进入,间隔 2 秒再放行下一批
}
}5.6 CountDownLatch(倒计数门闩)
适用场景:
- 主线程等待多个子线程全部完成后再继续(如并行计算汇总结果)
- 启动多个服务时,等待所有依赖服务就绪后再对外提供
- 一次性的「多等一」或「一等多」场景(计数归零后不可重置)
特点:初始化为一个计数器,子线程完成任务后
countDown()减一;等待方通过await()阻塞,直到计数归零。用完即失效,不可重复使用。
/**
* @className CountDownLatchDemo
* @description:
* @author legend
* @version 1.0
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int workerCount = 3;
CountDownLatch latch = new CountDownLatch(workerCount); // 倒计数初始为 3
for (int i = 0; i < workerCount; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 正在干活...");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
latch.countDown(); // 任务完成,计数器减 1
System.out.println(Thread.currentThread().getName() + " 干完了,剩余计数: " + latch.getCount());
}, "工人" + i).start();
}
// 主线程(Boss)等待计数器归零
latch.await();
System.out.println("Boss:所有工人都干完了,收工!");
// 注意:CountDownLatch 计数归零后不可重置复用
}
}5.7 CyclicBarrier(循环屏障)
适用场景:
- 多线程分阶段协同执行(如数据流水线:加载 → 处理 → 汇总)
- 需要重复使用屏障(CountDownLatch 只能用一次,CyclicBarrier 可循环触发)
- 所有参与线程都到达某一同步点后,才继续下一阶段的工作
特点:类似「多等多」的栅栏——N 个线程各自执行,都调用
await()到达屏障后,屏障自动触发(可选执行一个回调Runnable),然后 N 个线程继续下一阶段。屏障可重复使用。
/**
* @className CyclicBarrierDemo
* @description:
* @author legend
* @version 1.0
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 设置屏障点:3个线程都到达后,优先执行一个 Runnable(可选)
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("【屏障触发】所有线程都已到达,开始新一轮任务!");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 到达屏障 1");
barrier.await(); // 等待其他线程
System.out.println(Thread.currentThread().getName() + " 通过屏障 1,执行第二阶段");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " 到达屏障 2");
barrier.await(); // 可重复使用
System.out.println(Thread.currentThread().getName() + " 通过屏障 2,全部结束");
} catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); }
}, "线程" + i).start();
}
// 观察:每次都是 3 个线程凑齐了才一起打印下一阶段,且屏障可复用(这里触发了两次)
}
}