Java并发核心:synchronized、ReentrantLock、Semaphore 全方位深度对比解析
在 Java 并发编程体系中,synchronized、ReentrantLock、Semaphore(信号量)是控制线程竞争、实现资源隔离的三大核心工具。其中 synchronized 是 JVM 原生锁,ReentrantLock 是 JDK 层面的可重入显式锁,二者主要用于独占式资源竞争;而 Semaphore(信号量)是共享式限流工具,支持多线程同时访问资源。
很多开发者仅停留在“synchronized 简单、Lock 灵活”的浅层认知,实际开发中经常出现锁使用不当、并发限流失效、死锁等问题。本文将从底层原理、核心特性、使用场景、代码实战、优缺点对比、适用场景选型六个维度,彻底讲透三者的区别与用法,帮你精准适配各类并发场景。
一、synchronized:JVM 原生隐式锁
1. 核心定义
synchronized 是 Java 内置关键字,底层基于C++实现,属于隐式锁,锁的获取和释放完全由 JVM 自动控制,无需手动干预。它可以修饰方法、代码块,主要用于保证原子性、可见性、有序性三大并发特性,是解决线程安全问题的基础方案。
2. 底层原理
synchronized 底层依赖 JVM 监视器(Monitor)实现,基于 对象头 Mark Word 存储锁状态,存在四种锁升级状态(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),锁升级过程不可逆,有效降低了锁竞争的性能开销:
-
偏向锁:单线程访问资源时,JVM 记录线程 ID,无锁竞争,性能极高
-
轻量级锁:少量线程交替竞争,基于 CAS 自旋实现,无阻塞
-
重量级锁:多线程激烈竞争,线程阻塞挂起,依赖操作系统内核实现,性能最低
3. 核心特性
-
隐式加锁解锁:进入同步代码块自动加锁,退出代码块/方法、抛出异常时自动释放锁,无需手动操作
-
天然可重入:同一线程可多次获取同一把锁,不会出现自死锁
-
默认非公平锁:锁释放后,后续竞争线程随机抢占,不保证线程执行顺序
-
不可中断:线程阻塞等待锁时,无法被中断,只能一直等待获取锁
-
不支持超时等待:没有超时机制,竞争不到锁会永久阻塞
4. 代码实战
// 修饰代码块(推荐,锁粒度更细)
public class SyncDemo {
private int count = 0;
private final Object lock = new Object();
public void increment() {
// 隐式加锁
synchronized (lock) {
count++;
}
// 隐式解锁
}
}
// 修饰实例方法(锁当前对象 this)
public synchronized void add() {
count++;
}
// 修饰静态方法(锁类对象)
public static synchronized void staticAdd() {
count++;
}
5. 优缺点总结
优点:使用简单、零代码侵入、JVM 底层优化完善、无需手动释放锁、不会出现锁泄漏。
缺点:灵活性极差、不支持中断、不支持超时、不支持公平锁手动配置、锁粒度无法精细化控制,激烈竞争下性能较差。
二、ReentrantLock:JDK 显式可重入锁
1. 核心定义
ReentrantLock 是 java.util.concurrent.locks 包下的显式锁,基于 AQS(AbstractQueuedSynchronizer)队列同步器实现,是 synchronized 的增强替代品。需要开发者手动调用加锁、解锁方法,拥有远超 synchronized 的灵活度。
2. 底层原理
核心基于 AQS 实现:通过 state 状态值 记录锁持有状态(0 无锁、>0 已加锁,可重入次数叠加),通过 CLH 双向队列 存储阻塞等待的线程,线程竞争失败后进入队列排队,锁释放后唤醒队列头部线程竞争锁。
3. 核心特性(核心优势)
-
显式加锁解锁:手动 lock() 加锁、unlock() 解锁,粒度完全可控
-
可重入:和 synchronized 一致,同一线程可多次获取锁
-
支持公平/非公平锁:构造方法传 true 为公平锁(线程排队有序执行),默认非公平
-
可中断锁等待:lockInterruptibly() 方法可响应线程中断,避免永久阻塞
-
超时锁机制:tryLock(timeout) 可设置等待超时时间,超时直接放弃竞争
-
绑定多个条件变量:可通过 newCondition() 创建多个 Condition,精准唤醒指定线程,精准控制线程等待/唤醒
4. 代码实战(含核心用法)
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
private int count = 0;
// 可指定公平/非公平锁:true=公平,false=非公平(默认)
private final ReentrantLock lock = new ReentrantLock(true);
// 自定义条件变量
private final Condition condition = lock.newCondition();
// 基础加锁
public void increment() {
lock.lock(); // 手动加锁
try {
count++;
} finally {
lock.unlock(); // 必须在finally手动解锁,防止锁泄漏
}
}
// 超时等待锁
public boolean tryIncrement() throws InterruptedException {
// 等待1秒,获取不到锁直接返回false
if (lock.tryLock(1000, java.util.concurrent.TimeUnit.MILLISECONDS)) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false;
}
// 精准线程等待唤醒
public void waitThread() throws InterruptedException {
lock.lock();
try {
condition.await(); // 线程等待
} finally {
lock.unlock();
}
}
public void signalThread() {
lock.lock();
try {
condition.signal(); // 精准唤醒等待线程
} finally {
lock.unlock();
}
}
}
5. 优缺点总结
优点:灵活性极强、支持公平锁、中断等待、超时机制、多条件精准唤醒,高并发场景性能优于 synchronized。
缺点:需要手动加锁解锁,编码繁琐;必须在 finally 解锁,否则极易出现锁泄漏;需要开发者手动控制锁逻辑,容错率低。
三、Semaphore:信号量
1. 核心定义
Semaphore(信号量)同样基于 AQS 实现,和前两者独占锁不同,它是共享式同步工具。核心作用是控制并发访问资源的线程数量,实现限流、资源池控制、接口限流等场景。
简单理解:synchronized/ReentrantLock 是 一把锁,同一时间只允许1个线程执行;Semaphore 是 多个通行证,同一时间允许N个线程并行执行。
2. 底层原理
基于 AQS 的共享模式实现:初始化时设置许可数量(permits),对应 AQS 的 state 值。线程调用 acquire() 申请许可,state 减 1;线程执行完毕调用 release() 释放许可,state 加 1。当 state=0 时,后续线程阻塞等待,直到有许可释放。
3. 核心特性
-
共享式并发控制:支持多线程同时访问资源,核心用于限流而非互斥
-
支持公平/非公平模式:可指定线程排队规则
-
可批量申请/释放许可:支持一次性获取/释放多个许可
-
支持中断、超时机制:acquire() 可响应中断,tryAcquire() 支持超时
-
无锁重入概念:核心是许可计数,不区分线程,可重复申请
4. 常用核心方法
|
方法 |
作用 |
|---|---|
|
acquire() |
获取1个许可,无许可则阻塞,可响应中断(P操作) |
|
acquire(int n) |
批量获取n个许可 |
|
tryAcquire() |
尝试获取许可,获取失败直接返回false,不阻塞 |
|
tryAcquire(timeout) |
超时获取许可,超时未获取则放弃 |
|
release() |
释放1个许可(V操作) |
|
release(int n) |
批量释放n个许可 |
5. 代码实战:接口限流
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
// 初始化许可数:同一时间最多允许5个线程并发执行(限流5)
private static final Semaphore SEMAPHORE = new Semaphore(5);
public static void main(String[] args) {
// 模拟20个并发请求
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
try {
// 申请许可
SEMAPHORE.acquire();
System.out.println(Thread.currentThread().getName() + " 执行任务,当前并发数:" + (5 - SEMAPHORE.availablePermits()));
// 模拟业务耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
SEMAPHORE.release();
}
}, "线程" + i).start();
}
}
}
运行效果:始终最多5个线程同时执行,完美实现接口、资源、数据库连接池的并发限流。
6. 典型使用场景
-
接口限流、秒杀限流(限制最大并发请求数)
-
资源池控制(数据库连接池、线程池、硬件资源占用)
-
多线程分段任务、并行任务控速
-
实现简单的生产者消费者模型
四、synchronized vs ReentrantLock 核心深度对比
二者核心定位一致:独占互斥锁,保证同一时间只有一个线程执行临界区代码,解决线程安全问题,核心差异在灵活性、性能、功能特性。
|
对比维度 |
synchronized |
ReentrantLock |
|---|---|---|
|
实现方式 |
JVM 原生隐式锁,关键字级别 |
JDK 显式锁,基于 AQS 实现 |
|
加锁解锁 |
自动加锁、自动解锁,无需手动操作 |
手动 lock()、unlock(),需 finally 释放 |
|
锁类型 |
默认非公平锁,不支持公平锁 |
支持公平/非公平锁,可自由配置 |
|
中断等待 |
不支持,阻塞线程无法被中断 |
支持 lockInterruptibly() 中断等待 |
|
超时机制 |
不支持,永久阻塞等待 |
支持 tryLock() 超时放弃竞争 |
|
条件变量 |
只有1个隐式条件,只能整体唤醒/随机唤醒 |
支持多 Condition,精准唤醒指定线程 |
|
可重入性 |
支持可重入 |
支持可重入 |
|
性能 |
JDK1.6 后优化极高,低竞争场景性能优秀 |
高竞争场景性能优于 synchronized |
|
容错性 |
极高,无锁泄漏风险 |
低,手动解锁失误会导致锁泄漏、死锁 |
五、三者核心定位与选型区别(重点)
很多开发者混淆三者的核心用途,本质区别一句话总结:
-
synchronized:简单独占互斥,低并发、简单线程安全场景首选
-
ReentrantLock:复杂独占互斥,高并发、需要灵活锁控制场景首选
-
Semaphore:共享限流,控制并发线程数,不做互斥,只做流量控制
最终选型规则
-
简单线程安全、代码简洁优先:无脑用 synchronized(普通变量计数、简单同步方法)
-
需要公平锁、超时、中断、精准线程唤醒:必须用 ReentrantLock
-
需要控制并发数量、接口限流、资源池管控:必须用 Semaphore
-
绝对不要混用:独占锁用于“互斥排他”,信号量用于“并发限流”,场景完全不同
六、常见面试高频问题总结
-
synchronized 和 ReentrantLock 区别? 核心是隐式/显式、功能灵活性、公平锁、中断、超时机制的差异,上文对比表全覆盖。
-
为什么 ReentrantLock 要在 finally 解锁? 防止业务代码异常导致无法解锁,造成锁泄漏,后续线程永久阻塞。
-
Semaphore 和锁的本质区别? 锁是独占模式(1线程执行),Semaphore 是共享模式(N线程并行),用于限流而非互斥。
-
Semaphore 许可数可以设置为1吗? 可以,此时等价于非公平独占锁,但性能和功能不如原生锁,不推荐使用。
-
可重入锁的意义? 避免同一线程多次获取同一锁时出现自死锁,适配嵌套同步场景。
七、全文总结
1. synchronized:JVM 原生隐式独占锁,简单稳定、零出错成本,适配低并发、简单互斥场景,是日常开发首选基础锁。
2. ReentrantLock:AQS 实现的显式独占锁,功能全面、灵活性拉满,适配高并发、复杂锁控制场景,是 synchronized 的增强替代方案。
3. Semaphore:AQS 共享式同步工具,核心能力是并发限流,用于控制并行线程数,和互斥锁无竞争关系,是接口限流、资源管控的核心工具。
掌握三者的底层原理和场景差异,才能在并发开发中精准选型,既避免过度设计,也能解决高并发下的线程安全、限流、阻塞等核心问题。
更多推荐


所有评论(0)