💡 前言
在多线程并发编程中,线程安全问题始终是开发者需要重点关注的核心内容之一。Java 提供了多种机制来实现同步控制,其中最常用的两种方式是:
- 使用
synchronized
关键字 - 使用
java.util.concurrent.locks.Lock
接口(如ReentrantLock
)
虽然两者都能实现线程同步功能,但它们在使用方式、灵活性、可扩展性以及性能优化方面存在显著差异。
本文将从底层原理、语法结构、使用场景、优缺点、最佳实践等多个维度对 synchronized
和 Lock
进行全面深入的解析,并通过大量代码示例帮助你更好地理解它们之间的区别与联系。
📌 一、synchronized
关键字详解
1. 基本概念
synchronized
是 Java 内置的关键字,用于保证多个线程对共享资源访问时的互斥性和可见性。它可以修饰方法或代码块,确保同一时刻只有一个线程可以执行被同步的代码。
2. 使用方式
(1)修饰实例方法
java
public synchronized void method() {
// 同步整个方法体
}
此时锁对象是当前类的实例(即 this
)。
(2)修饰静态方法
java
public static synchronized void staticMethod() {
// 同步静态方法
}
此时锁对象是当前类的 Class 对象(即 ClassName.class
)。
(3)修饰代码块(推荐)
java
public void method() {
synchronized (this) {
// 同步代码块
}
}
更灵活,可以指定任意对象作为锁,推荐使用这种方式以减少锁定范围。
3. 特性总结
特性 | 描述 |
---|---|
自动释放锁 | JVM 在同步块执行结束后自动释放锁 |
不可中断 | 等待获取锁的线程无法被中断 |
非公平锁 | 多个线程竞争时,不保证先等待的线程优先获得锁 |
可重入性 | 支持同一个线程多次获取同一把锁 |
🔑 二、Lock
接口详解(以 ReentrantLock
为例)
1. 基本概念
Lock
是 Java 5 引入的一个接口,位于 java.util.concurrent.locks
包下。常见的实现类有:
ReentrantLock
:可重入锁ReadWriteLock
:读写分离锁(实现类为ReentrantReadWriteLock
)
相比 synchronized
,Lock
更加灵活和强大,提供了更多高级功能。
2. 使用方式
java
Lock lock = new ReentrantLock();
lock.lock(); // 手动加锁
try {
// 临界区逻辑
} finally {
lock.unlock(); // 必须放在 finally 块中释放锁
}
⚠️ 注意:必须手动调用 unlock()
,否则可能导致死锁!
3. 核心特性
特性 | 描述 |
---|---|
手动管理锁 | 需要显式调用 lock() 和 unlock() |
可中断等待 | 支持线程在等待锁的过程中响应中断(lockInterruptibly() ) |
超时获取锁 | 支持尝试获取锁并设置超时时间(tryLock(long time, TimeUnit unit) ) |
公平锁/非公平锁 | 构造函数可选择是否启用公平锁 |
条件变量支持 | 提供 Condition 接口,实现更细粒度的线程通信 |
🤔 三、synchronized
与 Lock
的核心区别对比表
功能 | synchronized |
Lock |
---|---|---|
加锁方式 | 自动加锁、解锁 | 手动加锁、解锁 |
锁类型 | 非公平锁 | 可选公平/非公平 |
可中断 | ❌ 不支持 | ✅ 支持 |
超时机制 | ❌ 不支持 | ✅ 支持 |
尝试获取锁 | ❌ 不支持 | ✅ 支持 |
条件变量 | ❌ 不支持 | ✅ 支持 |
性能优化 | JDK 1.6+ 已优化 | 更适合高并发场景 |
适用场景 | 简单同步需求 | 复杂并发控制场景 |
🎯 四、使用场景对比与建议
场景 | 推荐使用 | 说明 |
---|---|---|
简单方法或代码块同步 | synchronized |
实现简单,无需手动释放锁 |
高并发、复杂同步控制 | Lock |
提供更多控制选项,如公平锁、尝试锁等 |
需要线程中断响应 | Lock |
synchronized 不支持中断等待 |
需要条件变量配合 | Lock |
Condition 可替代传统的 wait/notify |
需要超时获取锁 | Lock |
tryLock() 方法非常实用 |
🧪 五、实战案例分析
案例 1:带超时的锁获取(适用于防止死锁)
java
Lock lock = new ReentrantLock();
boolean isLocked = false;
try {
isLocked = lock.tryLock(3, TimeUnit.SECONDS);
if (isLocked) {
try {
// 执行业务逻辑
} finally {
lock.unlock();
}
} else {
System.out.println("未能在3秒内获取到锁");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程被中断");
}
案例 2:使用 Condition
实现生产者-消费者模型
java
class BoundedQueue {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
public BoundedQueue(int capacity) {
this.capacity = capacity;
}
public void put(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 等待队列不满
}
queue.add(value);
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待队列不空
}
return queue.poll();
} finally {
lock.unlock();
}
}
}
🧠 六、底层原理简析(进阶)
1. synchronized
的底层实现
在 JVM 层面,synchronized
是基于 Monitor(监视器)机制实现的。每个 Java 对象都关联一个 Monitor,当线程进入同步块时,会尝试获取该对象的 Monitor,成功则进入,失败则阻塞。
JVM 对其进行了多项优化,包括:
- 偏向锁(Biased Locking)
- 轻量级锁(Lightweight Locking)
- 自旋锁(Spin Lock)
- 锁粗化(Lock Coarsening)
- 锁消除(Lock Elimination)
这些优化使得 synchronized
在现代 JVM 上表现优异。
2. ReentrantLock
的底层实现
ReentrantLock
底层依赖于 AbstractQueuedSynchronizer
(AQS)框架,是一个基于 CLH(Craig, Landin, and Hagersten)队列的同步工具。
它通过 CAS(Compare and Swap)操作和 volatile 变量实现线程安全,具有更高的可控性和灵活性。
🛠️ 七、最佳实践与注意事项
建议 | 说明 |
---|---|
优先考虑 synchronized |
如果只是简单的同步,优先使用 synchronized ,避免复杂代码 |
Lock 放在 finally 中释放 |
防止因异常导致死锁 |
使用 tryLock() 防止死锁 |
在某些情况下,尝试获取锁比无限等待更合理 |
避免嵌套锁 | 容易引发死锁,应尽量避免或使用工具检测 |
选择公平锁需谨慎 | 公平锁虽然保证顺序,但可能带来性能损耗 |
使用 Condition 替代 wait/notify |
更清晰、线程安全 |
📘 八、总结
项目 | synchronized |
Lock |
---|---|---|
是否内置 | ✅ 是 | ❌ 否 |
使用难度 | 简单 | 复杂 |
控制粒度 | 粗 | 细 |
功能丰富度 | 一般 | 强大 |
性能表现 | 好 | 更好(高并发) |
推荐用途 | 初学者、简单同步 | 高级用户、复杂并发控制 |
在实际开发中,两者各有优势 ,选择哪一个取决于具体的应用场景和团队技术栈。对于大多数中小型项目,synchronized
已经足够;而在需要更高并发控制能力的场景下,Lock
更具优势。
🎯 点赞、收藏、转发本文,让更多开发者受益!