在Java并发编程领域,线程同步是保障数据一致性的核心手段。面对高并发场景,开发者常面临一个关键抉择:是使用Java内置的synchronized关键字,还是选择JUC包中的ReentrantLock?本文将从底层实现、功能特性、性能表现及适用场景四个维度展开深度对比,为开发者提供清晰的决策依据。
一、底层实现:JVM级优化 vs AQS框架
1. synchronized的JVM级优化
synchronized是Java语言原生支持的同步机制,其实现经历了从重量级锁到轻量级锁的演进:
- JDK 1.6前:完全依赖操作系统互斥量(Mutex),线程阻塞需进入内核态,性能损耗显著。
- JDK 1.6后:引入偏向锁、轻量级锁(自旋锁)和适应性自旋:
- 偏向锁:单线程场景下,锁标记偏向首个获取它的线程,消除同步开销。
- 轻量级锁:通过CAS操作尝试获取锁,避免线程阻塞。
- 适应性自旋:根据锁竞争强度动态调整自旋次数,平衡CPU资源与线程唤醒成本。
代码示例:
java
1public class SynchronizedExample {
2 private int count = 0;
3 public synchronized void increment() {
4 count++; // 锁自动升级:偏向锁→轻量级锁→重量级锁
5 }
6}
7
2. ReentrantLock的AQS框架实现
ReentrantLock基于AbstractQueuedSynchronizer(AQS)实现,其核心逻辑包括:
- 状态变量(State):记录锁的重入次数,CAS操作保证原子性。
- 等待队列:双向链表结构,存储未获取锁的线程节点。
- 公平/非公平模式:通过
tryAcquire()方法控制锁分配顺序。
关键代码解析:
java
1// NonfairSync.tryAcquire() 非公平锁核心逻辑
2final boolean nonfairTryAcquire(int acquires) {
3 final Thread current = Thread.currentThread();
4 int c = getState();
5 if (c == 0) { // 锁空闲时直接CAS竞争
6 if (compareAndSetState(0, acquires)) {
7 setExclusiveOwnerThread(current);
8 return true;
9 }
10 } else if (current == getExclusiveOwnerThread()) { // 重入场景
11 int nextc = c + acquires;
12 if (nextc < 0) throw new Error("Maximum lock count exceeded");
13 setState(nextc);
14 return true;
15 }
16 return false;
17}
18
二、功能特性对比:基础同步 vs 高级控制
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁类型 | 非公平锁(默认) | 支持公平锁与非公平锁 |
| 锁释放 | 自动释放(方法/代码块结束) | 手动释放(需在finally块中调用unlock()) |
| 可中断性 | 不支持 | 支持lockInterruptibly()响应中断 |
| 超时获取 | 不支持 | 支持tryLock(long timeout, TimeUnit unit) |
| 条件变量 | 通过wait()/notify()实现 |
通过Condition对象实现多条件分组唤醒 |
| 锁状态查询 | 不支持 | 支持isLocked()、isHeldByCurrentThread() |
1. 公平锁与非公平锁
- 非公平锁(默认):新线程可直接尝试获取锁,可能“插队”成功,吞吐量更高但可能导致线程饥饿。
- 公平锁:通过
ReentrantLock(true)创建,严格按线程请求顺序分配锁,避免饥饿但性能略低。
应用场景:
- 非公平锁:适用于对响应时间敏感的场景(如金融交易系统)。
- 公平锁:适用于需要严格保证请求顺序的场景(如任务调度系统)。
2. 条件变量(Condition)
synchronized的条件等待需依赖Object.wait()/notify(),存在以下局限:
- 唤醒随机性:
notify()可能唤醒无关线程。 - 单一条件队列:无法区分不同等待条件。
ReentrantLock通过Condition对象实现更精细的控制:
java
1ReentrantLock lock = new ReentrantLock();
2Condition condition = lock.newCondition();
3
4public void awaitMethod() throws InterruptedException {
5 lock.lock();
6 try {
7 while (conditionNotMet) {
8 condition.await(); // 释放锁并等待
9 }
10 // 业务逻辑
11 condition.signalAll(); // 唤醒所有等待线程
12 } finally {
13 lock.unlock();
14 }
15}
16
三、性能表现:优化后的synchronized vs 灵活的ReentrantLock
1. 低竞争场景
- synchronized:在JDK 1.6+优化后,轻量级锁和偏向锁可显著减少同步开销,性能接近无锁代码。
- ReentrantLock:因需通过AQS维护等待队列,即使无竞争也存在额外开销。
测试数据:
- 单线程环境下,
synchronized的吞吐量比ReentrantLock高约15%。
2. 高竞争场景
- synchronized:锁竞争激烈时易升级为重量级锁,线程阻塞导致上下文切换成本增加。
- ReentrantLock:可通过以下策略优化性能:
- 公平锁:减少线程饥饿,但需权衡吞吐量。
- 分段锁:结合
Condition实现细粒度控制(如ReentrantReadWriteLock)。
测试数据:
- 16线程竞争时,
ReentrantLock的吞吐量比synchronized高约20%(通过合理配置公平性)。
四、适用场景决策树
1. 优先选择synchronized的场景
- 简单同步需求:如方法级同步、单变量原子操作。
- 代码简洁性优先:避免手动释放锁导致的死锁风险。
- 兼容性要求:需与
volatile、final等关键字协同保证可见性。
代码示例:
java
1public class Singleton {
2 private static volatile Singleton instance;
3 public static Singleton getInstance() {
4 if (instance == null) {
5 synchronized (Singleton.class) {
6 if (instance == null) {
7 instance = new Singleton();
8 }
9 }
10 }
11 return instance;
12 }
13}
14
2. 优先选择ReentrantLock的场景
- 需要高级功能:如可中断锁、超时获取、公平锁。
- 复杂同步逻辑:如多条件变量、读写锁分离。
- 性能敏感场景:通过AQS自定义同步器(如
CountDownLatch、Semaphore)。
代码示例:
java
1public class BoundedBuffer {
2 private final ReentrantLock lock = new ReentrantLock();
3 private final Condition notFull = lock.newCondition();
4 private final Condition notEmpty = lock.newCondition();
5 private final Object[] items = new Object[100];
6 private int putptr, takeptr, count;
7
8 public void put(Object x) throws InterruptedException {
9 lock.lock();
10 try {
11 while (count == items.length)
12 notFull.await();
13 items[putptr] = x;
14 if (++putptr == items.length) putptr = 0;
15 ++count;
16 notEmpty.signal();
17 } finally {
18 lock.unlock();
19 }
20 }
21}
22
五、总结与建议
- 默认选择
synchronized:其JVM级优化和简洁性适合大多数场景,尤其在JDK 1.6+后性能已与ReentrantLock接近。 - 特殊需求选
ReentrantLock:当需要公平锁、可中断锁、超时获取或复杂条件变量时,ReentrantLock是唯一选择。 - 避免过度设计:在性能差异不显著时,优先保障代码可读性和可维护性。
未来趋势:随着Java虚拟机的持续优化(如ZGC的并发标记算法),synchronized的性能可能进一步提升,而ReentrantLock的灵活性仍将在特定领域保持不可替代性。开发者需根据具体场景权衡选择,而非盲目追求“新技术”。