Java · #java#concurrency#synchronized#lock#juc

Java并发编程深入解析:从synchronized到Lock

2019.11.15 Java 8 min 3.4k
// 目录 · contents

1. 引言

在多核处理器成为标配的今天,并发编程已经是Java开发者的必备技能。Java从1.0版本就提供了synchronized关键字来解决线程同步问题,而在JDK 1.5中引入的java.util.concurrent(JUC)包则提供了更灵活、更强大的锁机制。

本文将从synchronized的底层实现原理出发,逐步深入到ReentrantLockReadWriteLockStampedLock,帮助读者全面理解Java中的锁机制,并在实际开发中做出正确的选择。

graph TB
    A[Java锁机制] --> B[内置锁 synchronized]
    A --> C[显式锁 Lock]
    B --> B1[对象锁]
    B --> B2[类锁]
    C --> C1[ReentrantLock]
    C --> C2[ReadWriteLock]
    C --> C3[StampedLock]
    C1 --> C1a[公平锁]
    C1 --> C1b[非公平锁]
    C2 --> C2a[ReentrantReadWriteLock]
    style A fill:#f9f,stroke:#333,stroke-width:2px

2. synchronized底层原理

2.1 Monitor机制

synchronized的实现依赖于JVM的Monitor(管程/监视器)机制。每个Java对象都可以关联一个Monitor对象,当线程执行synchronized代码块时,必须先获取该对象的Monitor。

stateDiagram-v2
    [*] --> EntryList: 线程请求锁
    EntryList --> Owner: 获取Monitor成功
    Owner --> WaitSet: 调用wait()
    WaitSet --> EntryList: 被notify()/notifyAll()唤醒
    Owner --> [*]: 释放Monitor
    EntryList --> EntryList: 竞争失败,继续等待

在HotSpot虚拟机中,Monitor由C++的ObjectMonitor实现,其核心数据结构包括:

  • **_owner**: 指向持有Monitor的线程
  • **_EntryList**: 等待获取锁的线程队列(阻塞队列)
  • **_WaitSet**: 调用了wait()方法后等待被唤醒的线程集合
  • **_count**: 重入计数器

2.2 monitorenter与monitorexit

synchronized修饰代码块时,编译后会生成monitorentermonitorexit字节码指令。我们通过一个例子来查看:

1
2
3
4
5
6
7
8
9
public class SyncDemo {
private final Object lock = new Object();

public void syncBlock() {
synchronized (lock) {
System.out.println("inside synchronized block");
}
}
}

使用javap -c SyncDemo.class反编译后,关键字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void syncBlock();
Code:
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter // 进入同步块,获取Monitor
7: getstatic #4 // Field java/lang/System.out
10: ldc #5 // String inside synchronized block
12: invokevirtual #6 // Method java/io/PrintStream.println
15: aload_1
16: monitorexit // 退出同步块,释放Monitor
17: goto 25
20: astore_2
21: aload_1
22: monitorexit // 异常路径也需要释放Monitor
23: aload_2
24: athrow
25: return

注意编译器生成了两条monitorexit指令:一条在正常退出路径,另一条在异常处理路径,确保任何情况下Monitor都能被正确释放。

2.3 锁升级过程

JDK 1.6之后,HotSpot对synchronized做了大量优化,引入了偏向锁、轻量级锁和重量级锁的升级机制:

graph LR
    A[无锁状态] -->|同一线程访问| B[偏向锁]
    B -->|其他线程竞争| C[轻量级锁/CAS]
    C -->|CAS自旋失败| D[重量级锁/Monitor]
    style A fill:#90EE90
    style B fill:#87CEEB
    style C fill:#FFD700
    style D fill:#FF6347
锁状态 适用场景 实现方式 性能开销
偏向锁 只有一个线程访问 对象头Mark Word记录线程ID 极低
轻量级锁 多线程交替执行,无竞争 CAS操作 + 自旋
重量级锁 多线程同时竞争 操作系统Mutex Lock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 演示synchronized在不同竞争条件下的锁升级
* 可通过JOL (Java Object Layout)工具观察对象头变化
*/
public class LockEscalationDemo {
private static final Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
// Phase 1: 偏向锁 - 单线程重复获取
for (int i = 0; i < 5; i++) {
synchronized (lock) {
// 只有main线程访问,保持偏向锁状态
}
}

// Phase 2: 轻量级锁 - 两个线程交替访问
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) {
// 交替执行,CAS竞争
}
}
});
t1.start();
t1.join();

// Phase 3: 重量级锁 - 多线程同时竞争
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
}
}

3. ReentrantLock详解

3.1 基本用法

ReentrantLock是JUC包中最核心的锁实现,相比synchronized提供了更丰富的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;

/**
* 标准用法:try-finally确保释放锁
*/
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 必须在finally中释放锁
}
}

/**
* 可超时获取锁,避免死锁
*/
public boolean tryIncrement(long timeout) throws InterruptedException {
if (lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false; // 超时未获取到锁
}

/**
* 可中断获取锁
*/
public void interruptibleIncrement() throws InterruptedException {
lock.lockInterruptibly(); // 等待过程中可被中断
try {
count++;
} finally {
lock.unlock();
}
}
}

3.2 公平锁与非公平锁

1
2
3
4
5
// 非公平锁(默认)- 允许插队,吞吐量更高
ReentrantLock unfairLock = new ReentrantLock(false);

// 公平锁 - 按请求顺序获取锁,避免饥饿
ReentrantLock fairLock = new ReentrantLock(true);
sequenceDiagram
    participant T1 as 线程1
    participant T2 as 线程2
    participant T3 as 线程3
    participant L as Lock

    Note over L: 非公平锁场景
    T1->>L: lock() 获取成功
    T2->>L: lock() 进入等待队列
    T1->>L: unlock() 释放锁
    T3->>L: lock() 此时刚好请求锁
    Note over T3,L: T3可能插队成功<br/>比T2先获取到锁
    T3->>L: 获取成功(插队)
    T3->>L: unlock()
    T2->>L: 获取成功

3.3 Condition条件变量

Conditionsynchronizedwait/notify机制的增强版,允许创建多个等待队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
* 使用Condition实现生产者-消费者模式
* 相比wait/notify,Condition支持多个等待队列,逻辑更清晰
*/
public class BoundedBuffer<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列未满条件
private final Condition notEmpty = lock.newCondition(); // 队列非空条件

public BoundedBuffer(int capacity) {
this.capacity = capacity;
}

public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 队列满了,等待消费者消费
}
queue.offer(item);
notEmpty.signal(); // 通知消费者可以消费了
} finally {
lock.unlock();
}
}

public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列空了,等待生产者生产
}
T item = queue.poll();
notFull.signal(); // 通知生产者可以生产了
return item;
} finally {
lock.unlock();
}
}
}

4. ReadWriteLock读写锁

在读多写少的场景下,使用独占锁会严重影响性能。ReadWriteLock允许多个读线程同时访问,但写操作是排他的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* 基于ReadWriteLock实现的线程安全缓存
* 读操作并发执行,写操作独占访问
*/
public class ReadWriteCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

public V get(K key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}

public void put(K key, V value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}

/**
* 锁降级:写锁 -> 读锁(安全的)
* 先获取写锁修改数据,然后降级为读锁继续使用数据
*/
public V computeIfAbsent(K key, java.util.function.Function<K, V> mappingFunction) {
rwLock.readLock().lock();
try {
V value = cache.get(key);
if (value != null) {
return value;
}
} finally {
rwLock.readLock().unlock();
}

// 读锁中未命中,升级为写锁
rwLock.writeLock().lock();
try {
// double-check: 可能其他线程已经写入
V value = cache.get(key);
if (value == null) {
value = mappingFunction.apply(key);
cache.put(key, value);
}
// 锁降级:在释放写锁前先获取读锁
rwLock.readLock().lock();
return value;
} finally {
rwLock.writeLock().unlock();
// 此时仍持有读锁,可以安全使用数据
rwLock.readLock().unlock();
}
}
}

5. StampedLock乐观读

JDK 8引入的StampedLockReadWriteLock的基础上增加了乐观读模式,在读多写少的场景下性能更优:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.locks.StampedLock;

/**
* StampedLock实现的二维点坐标
* 演示乐观读、悲观读和写锁的使用
*/
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();

/**
* 写操作:独占锁
*/
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}

/**
* 乐观读:不阻塞写操作,性能最优
* 如果在读取过程中发生了写操作,则回退到悲观读锁
*/
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 获取乐观读stamp,不加锁
double currentX = x, currentY = y; // 读取共享变量到局部变量

if (!sl.validate(stamp)) { // 检查读取期间是否有写操作
stamp = sl.readLock(); // 回退到悲观读锁
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}

6. 性能对比与选型建议

graph TB
    subgraph 选型决策树
        Q1{需要锁吗?} -->|是| Q2{读写比例?}
        Q1 -->|否| A1[无锁方案<br/>CAS/Atomic]
        Q2 -->|读多写少| Q3{能容忍乐观读失败重试?}
        Q2 -->|读写均衡| Q4{需要tryLock/中断?}
        Q2 -->|写多| Q4
        Q3 -->|是| A2[StampedLock]
        Q3 -->|否| A3[ReadWriteLock]
        Q4 -->|是| A4[ReentrantLock]
        Q4 -->|否| A5[synchronized]
    end
    style Q1 fill:#FFE4B5
    style A1 fill:#90EE90
    style A2 fill:#90EE90
    style A3 fill:#90EE90
    style A4 fill:#90EE90
    style A5 fill:#90EE90
特性 synchronized ReentrantLock ReadWriteLock StampedLock
可重入
公平性选择
可中断
超时获取
条件变量 1个(wait/notify) 多个Condition 多个Condition 不支持
乐观读
自动释放 否(需手动) 否(需手动) 否(需手动)

最佳实践

  1. 优先考虑synchronized:JDK 6之后性能已大幅优化,语法简洁且自动释放锁,能满足大多数场景。
  2. 需要高级特性时用ReentrantLock:可中断、可超时、公平锁、多Condition等场景。
  3. 读多写少用ReadWriteLock:典型场景如缓存、配置读取。
  4. 极致读性能用StampedLock:注意它不可重入且不支持Condition,需谨慎使用。
  5. 务必在finally中释放锁:Lock不像synchronized会自动释放,遗漏unlock()会导致死锁。
  6. 避免在锁内执行耗时操作:减小临界区范围,降低锁竞争。

7. 总结

Java的锁机制从synchronized的简洁到JUC Lock体系的灵活,为开发者提供了丰富的并发控制工具。理解每种锁的底层原理和适用场景,才能在实际项目中做出正确的选型:

  • synchronized经过JVM不断优化,在低竞争场景下性能优异,且使用最简单
  • ReentrantLock提供了丰富的API,是需要高级锁特性时的首选
  • ReadWriteLock通过读写分离提升了并发读的性能
  • StampedLock的乐观读模式在读远多于写的场景下能获得最佳性能

选择合适的锁不仅要考虑功能需求,还要结合具体的并发场景、竞争程度以及代码维护成本综合判断。


踩坑记录

参与公司第一个高并发项目——双十一大促秒杀系统时,我们用 synchronized 锁住了库存扣减方法。压测到 500 并发时 TPS 急剧下降,GC 日志里 STW 时间飙到 800ms。排查后发现是 synchronized 的偏向锁在高竞争场景下频繁撤销,JVM 升级为重量级锁的开销远超预期。最终改用 ReentrantLock + tryLock(timeout) 后,同等压力下 TPS 提升了 2.3 倍。

另一个坑是死锁排查。有一次线上服务卡死,jstack 看到 30 多个线程都处于 BLOCKED 状态,两个锁对象互相等待。根本原因是两个业务方法锁获取顺序不一致:方法 A 先锁 userLock 再锁 orderLock,方法 B 反过来。这种问题在 code review 很难发现,上线后才暴露。后来统一规定:多个锁必须按固定顺序获取,并写进开发规范文档。

实测结果

测试环境:4 核 8G,JDK 8,500 并发,秒杀场景压测

方案 TPS P99 延迟 GC STW 均值
synchronized 1,200 420ms 650ms
ReentrantLock 2,760 180ms 90ms

结论:低竞争场景两者差距不大;高竞争短临界区场景下 ReentrantLocktryLock 机制能避免线程阻塞挂起,优势非常明显。

我的看法

网上很多文章说 JDK 6 之后 synchronized 性能和 Lock 差不多了,但这个结论有前提:低竞争场景。在高竞争 + 短临界区的场景下,ReentrantLock 的优势依然显著。

我现在的选型原则是:先用 synchronized,遇到性能瓶颈或需要超时/中断/多 Condition 时再换 ReentrantLock。不要一上来就用 Lock,维护成本更高,忘记 finallyunlock 就是定时炸弹。

作者 · authorzt
发布 · date2019-11-15
篇幅 · length3.4k 字 · 8 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论