Java 多线程同步机制深度解析:从 synchronized 到 Lock

目录
- 一、多线程并发问题的根源
- 二、synchronized 关键字详解
- 2.1 同步代码块
- 2.2 同步方法
- 2.3 synchronized 的实现原理
- 2.4 synchronized 的锁升级过程
- 三、Lock 接口及其实现
- 3.1 ReentrantLock 的基本使用
- 3.2 Lock 接口的核心方法
- 3.3 ReentrantLock 的高级特性
- 四、synchronized 与 Lock 的对比分析
- 五、同步机制的最佳实践
- 六、总结
一、多线程并发问题的根源
在多核 CPU 时代,多线程编程成为提升程序性能的重要手段。然而,当多个线程同时操作共享资源时,就可能出现数据不一致的问题。
例如,两个线程同时对一个计数器进行递增操作,理想情况下:
- 线程 A 读取计数器值为 10
- 线程 A 将其加 1,值为 11
- 线程 A 将 11 写回内存
- 线程 B 读取计数器值为 11
- 线程 B 将其加 1,值为 12
- 线程 B 将 12 写回内存
但实际可能发生:
- 线程 A 读取计数器值为 10
- 线程 B 读取计数器值为 10
- 线程 A 将其加 1,值为 11 并写回
- 线程 B 将其加 1,值为 11 并写回
最终结果为 11 而非预期的 12,这就是典型的线程安全问题。

为了解决这类问题,Java 提供了多种同步机制,其中最常用的就是synchronized关键字和Lock接口。
二、synchronized 关键字详解
synchronized是 Java 内置的同步机制,它能够保证同一时刻只有一个线程进入临界区(被synchronized修饰的代码块或方法),从而避免线程安全问题。
2.1 同步代码块
同步代码块的语法格式如下:
synchronized (锁对象) {// 需要同步的代码
}
示例代码:
public class Counter {private int count = 0;private Object lock = new Object(); // 锁对象public void increment() {synchronized (lock) { // 同步代码块count++;}}public int getCount() {return count;}
}
同步代码块的特点:
- 灵活性高,可以精确控制需要同步的代码范围
- 锁对象可以是任意对象,但通常使用专门创建的对象作为锁
- 只有获取到锁对象的线程才能进入同步代码块
2.2 同步方法
同步方法是在方法声明中使用synchronized关键字:
// 同步实例方法
public synchronized void method() {// 需要同步的代码
}// 同步静态方法
public static synchronized void staticMethod() {// 需要同步的代码
}
示例代码:
public class Counter {private int count = 0;// 同步实例方法,锁对象是当前实例(this)public synchronized void increment() {count++;}// 同步实例方法public synchronized int getCount() {return count;}
}
同步方法的特点:
- 同步实例方法的锁是当前对象实例(this)
- 同步静态方法的锁是当前类的 Class 对象
- 整个方法体都处于同步控制之下

2.3 synchronized 的实现原理
synchronized的实现基于 Java 对象头和 Monitor(监视器锁)机制:
- Java 对象头:每个 Java 对象都有一个对象头,其中包含了锁的状态信息
- Monitor:每个对象都关联一个 Monitor,它是一个同步工具,负责管理等待进入临界区的线程
当线程进入synchronized代码块时,会执行以下操作:
- 尝试获取对象的 Monitor 所有权
- 如果获取成功,进入临界区执行代码
- 如果获取失败,线程进入阻塞状态,等待 Monitor 释放
- 退出
synchronized代码块时,释放 Monitor 所有权

2.4 synchronized 的锁升级过程
Java 6 及以后版本对synchronized进行了优化,引入了锁升级机制,从低到高依次为:
- 无锁状态:对象刚创建时,没有任何线程竞争
- 偏向锁:当只有一个线程访问同步代码块时,会记录线程 ID,避免每次获取和释放锁的开销
- 轻量级锁:当有多个线程交替访问时,使用 CAS 操作尝试获取锁,避免阻塞线程
- 重量级锁:当多个线程激烈竞争时,升级为重量级锁,此时会导致线程阻塞
锁升级的过程是不可逆的,只能从低级别向高级别升级。

三、Lock 接口及其实现
Lock接口是 Java 5 引入的同步机制,它提供了比synchronized更灵活的同步操作。ReentrantLock是Lock接口的主要实现类,具有可重入性。
3.1 ReentrantLock 的基本使用
ReentrantLock的使用步骤:
- 创建
ReentrantLock实例- 在需要同步的代码前调用
lock()方法获取锁- 在
finally块中调用unlock()方法释放锁(确保锁一定会被释放)
示例代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Counter {private int count = 0;private Lock lock = new ReentrantLock(); // 创建锁对象public void increment() {lock.lock(); // 获取锁try {count++; // 临界区代码} finally {lock.unlock(); // 释放锁,放在finally中确保一定会执行}}public int getCount() {return count;}
}
3.2 Lock 接口的核心方法
Lock接口定义了以下核心方法:
void lock():获取锁,如果锁被占用则阻塞boolean tryLock():尝试获取锁,成功返回 true,失败返回 false,不会阻塞boolean tryLock(long time, TimeUnit unit):在指定时间内尝试获取锁void lockInterruptibly():获取锁,但允许被中断void unlock():释放锁Condition newCondition():创建一个与该锁关联的条件对象

3.3 ReentrantLock 的高级特性
ReentrantLock相比synchronized提供了更多高级特性:
- 可中断锁:通过
lockInterruptibly()方法,线程在等待锁的过程中可以响应中断- 超时获取锁:通过
tryLock(long time, TimeUnit unit)方法,可以设置获取锁的超时时间- 公平锁:可以通过构造函数
ReentrantLock(boolean fair)创建公平锁,确保线程按照请求顺序获取锁- 条件变量:通过
newCondition()方法创建条件变量,实现更灵活的线程等待 / 唤醒机制
示例:使用条件变量实现生产者 - 消费者模式
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class ProducerConsumer {private static final int CAPACITY = 5;private Queue<Integer> queue = new ConcurrentLinkedQueue<>();private Lock lock = new ReentrantLock();private Condition notFull = lock.newCondition();private Condition notEmpty = lock.newCondition();// 生产者public void produce(int value) throws InterruptedException {lock.lock();try {// 队列满了则等待while (queue.size() == CAPACITY) {notFull.await(); // 等待队列不满}queue.add(value);System.out.println("生产: " + value);notEmpty.signal(); // 通知消费者队列非空} finally {lock.unlock();}}// 消费者public int consume() throws InterruptedException {lock.lock();try {// 队列空了则等待while (queue.isEmpty()) {notEmpty.await(); // 等待队列非空}int value = queue.poll();System.out.println("消费: " + value);notFull.signal(); // 通知生产者队列未满return value;} finally {lock.unlock();}}
}
四、synchronized 与 Lock 的对比分析
| 特性 | synchronized | Lock |
|---|---|---|
| 实现方式 | JVM 内置实现 | API 层面的实现 |
| 锁的释放 | 自动释放 | 必须手动释放(通常在 finally 中) |
| 锁的获取 | 阻塞获取 | 可阻塞获取、可尝试获取、可超时获取 |
| 可中断性 | 不可中断 | 可中断 |
| 公平性 | 非公平锁 | 可选择公平锁或非公平锁 |
| 条件变量 | 没有 | 提供 Condition 实现 |
| 性能 | 低竞争下性能好,Java 6 + 优化后性能提升明显 | 高竞争下性能更好 |
| 使用便捷性 | 简单,无需手动释放 | 相对复杂,需要手动释放 |

五、同步机制的最佳实践
- 优先使用无锁编程:尽量避免使用同步机制,可以通过不可变对象、ThreadLocal 等方式避免共享状态
- 最小化同步范围:只同步必要的代码块,减小锁的持有时间
- 避免嵌套锁:嵌套锁容易导致死锁
- 选择合适的锁类型:
- 简单场景优先使用
synchronized,代码简洁且不易出错 - 复杂场景(如需要中断、超时、公平性等)使用
Lock
- 简单场景优先使用
- 使用 try-finally 确保锁释放:对于
Lock,务必在 finally 块中释放锁 - 避免锁竞争:通过合理的设计减少线程间的锁竞争
- 考虑使用并发容器:JDK 提供的 ConcurrentHashMap 等并发容器内部实现了高效的同步机制
示例:错误与正确的 Lock 使用方式对比
// 错误方式:没有在finally中释放锁
public void badLockUsage() {lock.lock();if (condition) {return; // 提前返回,导致锁未释放}// 业务逻辑lock.unlock();
}// 正确方式:在finally中释放锁
public void goodLockUsage() {lock.lock();try {if (condition) {return; // 即使提前返回,finally仍会执行}// 业务逻辑} finally {lock.unlock(); // 确保锁一定会被释放}
}
六、总结
Java 提供了synchronized和Lock两种主要的同步机制,它们各有优缺点:
synchronized是 Java 语言内置的同步机制,使用简单,无需手动释放锁,在 Java 6 之后经过优化性能有了很大提升,适合大多数简单的同步场景。
Lock接口(主要是ReentrantLock)提供了更灵活的同步操作,支持可中断、超时获取、公平锁和条件变量等高级特性,适合复杂的同步场景。
在实际开发中,应根据具体需求选择合适的同步机制。对于大多数情况,synchronized已经足够好用且性能表现良好;当需要更灵活的同步控制时,再考虑使用Lock接口。
掌握这两种同步机制的原理和使用场景,是 Java 多线程编程的基础,也是编写高效、线程安全的并发程序的关键。
希望本文能帮助你深入理解 Java 多线程同步机制,如果有任何疑问或建议,欢迎在评论区留言讨论!

