Java ReentrantLock和synchronized的相同点与区别
1. 核心概念与定位
-
synchronized:
Java 内置的关键字,属于 JVM 层面的隐式锁。通过在方法或代码块上声明,自动实现锁的获取与释放,无需手动操作。设计目标是提供简单易用的基础同步能力,适合大多数常规同步场景。 -
ReentrantLock:
位于java.util.concurrent.locks包下的类,实现了Lock接口,属于显式锁。需要通过lock()手动获取锁,unlock()手动释放锁(通常在finally块中执行)。设计目标是提供更灵活的同步控制,满足复杂场景需求。
2. 核心共性
- 可重入性:两者都是可重入锁,即同一线程可以多次获取同一把锁(如递归调用同步方法),不会产生死锁。
- 线程互斥:核心功能一致,都能保证同一时间只有一个线程进入临界区,实现线程安全。
3. 关键区别
(1)锁的获取与释放方式
- synchronized 是隐式锁,自动获取和释放,无需手动操作
- ReentrantLock 是显式锁,必须通过
lock()获取锁,unlock()释放锁 - ReentrantLock 必须在 finally 块中释放锁,否则可能因异常导致锁无法释放,造成死锁
- synchronized 更简洁,ReentrantLock 更灵活但需更小心使用
// synchronized 示例
public class SynchronizedExample {
private int count = 0;
// 隐式获取和释放锁
public synchronized void increment() {
count++; // 临界区
}
}
// ReentrantLock 示例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
// 显式获取和释放锁
public void increment() {
lock.lock(); // 显式获取锁
try {
count++; // 临界区
} finally {
lock.unlock(); // 显式释放锁(必须在finally中)
}
}
}
(2)尝试获取锁与超时机制
ReentrantLock 支持超时获取锁:
- ReentrantLock 的
tryLock()方法可以尝试获取锁而不阻塞,或设置超时时间 - 超时机制可以避免线程无限期等待锁,提高系统的灵活性和稳定性
synchronized 不支持超时机制:
- synchronized 一旦开始等待,就必须等到锁释放,无法主动放弃
// ReentrantLock 支持尝试获取锁和超时
public class LockWithTimeout {
private final ReentrantLock lock = new ReentrantLock();
public boolean tryDoSomething() throws InterruptedException {
// 尝试获取锁,最多等待1秒
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 执行操作
return true;
} finally {
lock.unlock();
}
}
return false; // 获取锁失败
}
}
// synchronized 不支持超时,必须一直等待
public class SynchronizedNoTimeout {
public synchronized void doSomething() {
// 无法设置超时,必须等待锁释放
}
}
(3)可中断特性
ReentrantLock 支持中断:
- ReentrantLock 的
lockInterruptibly()方法允许线程在等待锁的过程中响应中断 - 可中断特性在需要取消任务或关闭服务时非常有用
synchronized 不支持中断:
- synchronized 等待锁的过程无法被中断,可能导致线程一直阻塞
// ReentrantLock 支持中断
public class InterruptibleLock {
private final ReentrantLock lock = new ReentrantLock();
public void doWithInterrupt() throws InterruptedException {
// 可被中断的锁获取
lock.lockInterruptibly();
try {
// 执行操作
} finally {
lock.unlock();
}
}
}
// synchronized 不支持中断
public class SynchronizedNotInterruptible {
public synchronized void doSomething() {
// 即使线程被中断,仍会继续等待锁
}
}
(4)公平锁实现
什么是 “公平锁” 与 “非公平锁”:
- 公平锁:线程获取锁的顺序严格遵循 “请求锁的先后顺序”(FIFO),先请求的线程一定先拿到锁,不会出现 “后请求的线程插队” 的情况。
- 非公平锁:线程获取锁时不严格遵循请求顺序,允许 “插队”—— 即使队列中有等待的线程,新到达的线程也可以尝试直接抢占锁,抢占失败后再进入队列排队。
ReentrantLock 可实现公平锁:
- ReentrantLock 可以通过构造函数参数
true创建公平锁 - 公平锁保证线程获取锁的顺序与请求顺序一致,避免线程饥饿
- 非公平锁允许线程 "插队" 获取锁,可能导致某些线程长时间等待,但性能更高
synchronized 只能是非公平锁:
- synchronized 始终是非公平锁,无法改为公平锁
// ReentrantLock 可创建公平锁
public class FairLockExample {
// 公平锁:按请求顺序获取锁
private final ReentrantLock fairLock = new ReentrantLock(true);
public void doWithFairLock() {
fairLock.lock();
try {
// 执行操作
} finally {
fairLock.unlock();
}
}
}
// synchronized 只能是非公平锁
public class SynchronizedNonFair {
// 无法设置为公平锁,始终是非公平的
public synchronized void doSomething() {
// 执行操作
}
}
为什么synchronized 非公平,不支持公平锁?:
synchronized 的实现依赖 JVM 底层的 对象监视器(Monitor),其锁分配逻辑本质是 “优先让当前可执行的线程获取锁,减少线程切换开销”,具体体现在两个关键场景:
场景 1:新线程请求锁时,直接 “插队” 抢占
当一个线程 A 释放锁时,JVM 并不会立刻唤醒等待队列中最前面的线程 B(公平锁逻辑),而是会先检查 当前是否有新线程 C 正在请求锁。
如果有新线程 C,JVM 会允许 C 直接抢占锁(无需进入等待队列),只有当没有新线程时,才会唤醒队列中的 B。
为什么这么做?
线程从 “等待状态” 被唤醒,需要经历 内核态→用户态 的切换(操作系统级操作),这个过程开销较大;而新线程 C 本身处于 “运行态”,直接让它获取锁可以避免一次线程切换,显著提升性能。
例如:
- 线程 A 释放锁时,等待队列中有线程 B(已等待 10ms);
- 此时线程 C 刚执行到
synchronized代码块,请求锁; - JVM 会让 C 直接拿到锁,B 继续等待;
- 只有当 A 释放锁时没有新线程,才唤醒 B。
场景 2:线程重入时,无需排队
synchronized 是 可重入锁(同一线程可多次获取同一把锁),而重入逻辑本身就是 “非公平” 的 —— 线程再次请求已持有的锁时,无需进入等待队列,直接成功获取。
这是因为:线程持有锁时,本身就有权限访问临界区,重入时跳过排队是合理的,且能避免 “自己等自己” 的死锁问题。但从公平性角度看,这相当于 “持有锁的线程插队”,优先于队列中的其他线程。
例如:
- 线程 A 已持有锁,执行到一个嵌套的
synchronized代码块; - 此时等待队列中有线程 B;
- 线程 A 无需排队,直接重入锁,B 继续等待。
公平性的性能代价:
代价 1:强制线程切换,增加开销
公平锁要求严格按 “请求顺序” 分配锁,这意味着:
- 当锁释放时,必须唤醒等待队列中最前面的线程(不能让新线程插队);
- 被唤醒的线程需要从 “等待态” 切换到 “运行态”,这个过程涉及操作系统内核操作,开销远大于 “新线程直接抢占”。
代价 2:锁竞争激烈时,吞吐量下降
公平锁会导致 “等待队列越长,新线程越难获取锁”,即使新线程能快速执行完临界区,也必须排队。
例如:
- 等待队列中有 10 个线程,每个线程执行临界区需要 100ms;
- 此时有一个新线程,临界区仅需 1ms;
- 公平锁下,新线程必须排在第 11 位,等待 10×100ms=1000ms 后才能执行,总耗时 1001ms;
- 非公平锁下,新线程可以直接抢占,总耗时 1ms(新线程)+ 10×100ms(队列线程)= 1001ms?不 —— 实际是新线程执行 1ms 后释放锁,队列中的第一个线程立刻执行,总耗时会更短(1ms + 100ms + ...),因为减少了一次线程切换的等待。
4.总结
ReentrantLock 和 synchronized 的核心区别可以概括为:
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 锁操作方式 | 显式(lock/unlock) | 隐式(自动获取释放) |
| 灵活性 | 高,支持多种获取方式 | 低,固定的获取方式 |
| 超时获取 | 支持 | 不支持 |
| 可中断性 | 支持 | 不支持 |
| 公平性 | 可选择 | 仅非公平 |
| 锁状态查询 | 可查询(isLocked () 等) | 不可查询 |
| 使用复杂度 | 较高,需手动释放 | 低,不易出错 |
synchronized 是 “简单易用的基础方案”,ReentrantLock 是 “灵活可控的高级方案”。现代 Java 版本中两者性能差异不大,选择时主要依据功能需求:简单场景用 synchronized,复杂场景用 ReentrantLock。
适用场景:
-
优先用 synchronized:
简单同步场景(如普通方法 / 代码块同步)、追求代码简洁性、低并发场景。
优势:无需手动释放锁,减少出错概率,JVM 持续优化(如偏向锁、轻量级锁)。 -
优先用 ReentrantLock:
需要超时获取锁、可中断锁、公平锁的场景;需要多个条件变量精确控制线程唤醒;复杂同步逻辑(如生产者 - 消费者模型的精细化控制)。
更多推荐


所有评论(0)