Java线程锁
在多线程编程中,当多个线程共享资源时,"并发操作导致数据不一致"是最常见的问题。例如两个线程同时执行count++,最终结果可能比预期少1——这就是竞态条件(Race Condition)。线程锁作为解决该问题的核心机制,通过控制共享资源的访问权限,保证多线程操作的安全性。
# 一、线程锁的核心概念
# 1.1 什么是线程锁?
线程锁是一种同步机制,用于保证多个线程对共享资源的互斥访问。它通过限制"同一时间只有一个线程能操作共享资源",避免并发操作导致的数据错乱。
# 1.2 为什么需要线程锁?
线程共享进程的内存空间,当多个线程同时操作共享变量、对象等资源时,单条指令(如count++)可能被拆分为多个步骤(读取→修改→写入)。若步骤被其他线程打断,会导致结果异常。
示例:竞态条件导致的错误
public class RaceConditionDemo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 两个线程同时执行1000次count++
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) count++;
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) count++;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:" + count); // 预期2000,实际可能小于2000
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
原因:线程1读取count=500后,线程2也读取count=500,两者分别加1后写入,最终count=501(而非502)。
# 二、线程锁的分类
根据设计思想、粒度和特性,线程锁可分为以下类型:
# 2.1 按核心策略:悲观锁 vs 乐观锁
悲观锁
假设线程一定会发生并发冲突,因此在访问资源前必须先获取锁,全程独占资源。- 典型例子:Java的
Synchronized、ReentrantLock,数据库的行锁/表锁。 - 优点:安全性高;缺点:频繁加锁释放锁会增加性能开销。
- 典型例子:Java的
乐观锁
假设线程不会发生冲突,因此不主动加锁,仅在更新资源时检查是否被其他线程修改。- 实现方式:版本号机制(如数据库乐观锁)、CAS(Compare-And-Swap,比较并交换)。
- 优点:无锁竞争,性能高;缺点:冲突频繁时会不断重试,反而降低效率。
# 2.2 按互斥粒度:独占锁 vs 共享锁
独占锁(排他锁)
同一时间只允许一个线程获取锁,其他线程必须等待。- 典型例子:
Synchronized、ReentrantLock(默认模式)。 - 适用场景:写操作(修改共享资源)。
- 典型例子:
共享锁
允许多个线程同时获取锁,但仅支持读操作;写操作仍需独占锁。- 典型例子:
ReentrantReadWriteLock.ReadLock(读锁共享,写锁独占)。 - 优点:提高读操作的并发效率(多线程可同时读)。
- 典型例子:
# 2.3 按可重入性:可重入锁 vs 不可重入锁
可重入锁
同一线程可多次获取同一把锁,锁会记录"持有次数",释放次数需与获取次数一致(避免自己锁死自己)。- 典型例子:
Synchronized、ReentrantLock(名称中"Reentrant"即表示可重入)。 - 示例:递归调用同步方法时,不会因重复获取锁而死锁。
- 典型例子:
不可重入锁
同一线程多次获取同一把锁会导致死锁(线程已持有锁,再次获取时会阻塞自己)。- 典型例子:早期的
SimpleLock(自定义简易锁,未实现重入逻辑)。
- 典型例子:早期的
# 2.4 按公平性:公平锁 vs 非公平锁
公平锁
按线程请求锁的先后顺序分配锁(先到先得),避免线程饥饿(长期得不到锁)。- 典型例子:
ReentrantLock(true)(构造参数传true)。 - 优点:公平性高;缺点:需维护等待队列,性能较低。
- 典型例子:
非公平锁
线程获取锁时不按顺序,允许"插队"(刚释放锁的线程可能立即再次获取锁)。- 典型例子:
Synchronized(默认非公平)、ReentrantLock(false)(默认)。 - 优点:性能高(减少线程切换开销);缺点:可能导致部分线程长期等待。
- 典型例子:
# 三、Java 内置锁:Synchronized
synchronized是Java最常用的内置锁机制,可保证代码块/方法的原子性、可见性和有序性,无需手动释放锁(JVM自动管理)。
# 3.1 用法:三种同步场景
synchronized的锁对象由修饰范围决定,具体用法如下:
| 用法 | 锁对象 | 作用范围 | 示例代码 |
|---|---|---|---|
| 修饰静态方法 | 当前类的Class对象 | 整个静态方法 | public static synchronized void staticMethod() { ... } |
| 修饰实例方法 | 当前实例对象(this) | 整个实例方法 | public synchronized void instanceMethod() { ... } |
| 修饰代码块 | 自定义对象(如lock) | 代码块内部 | synchronized (lock) { ... } (lock可为任意Object对象) |
注意:
- 静态方法锁是类级别的,所有实例共享同一把锁;
- 实例方法锁是对象级别的,不同实例的锁相互独立(不会互斥)。
# 3.2 底层实现:字节码视角
synchronized的底层依赖JVM的监视器(Monitor) 机制,通过字节码指令或标志位实现锁的获取与释放。
# 3.2.1 同步代码块:monitorenter与monitorexit
编译后,同步代码块会生成monitorenter(获取锁)和monitorexit(释放锁)指令:
public class SyncBlockDemo {
private final Object lock = new Object();
public void test() {
synchronized (lock) { // 同步代码块
System.out.println("临界区代码");
}
}
}
2
3
4
5
6
7
8
反编译字节码(javap -v SyncBlockDemo.class)核心片段:
Code:
0: aload_0 // 加载锁对象lock
1: getfield #2 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter // 尝试获取锁(进入Monitor)
7: getstatic #3 // 执行临界区代码(打印逻辑)
37: aload_1
38: monitorexit // 正常释放锁(退出Monitor)
39: goto 47
43: astore_2
44: aload_1
45: monitorexit // 异常时也释放锁(避免锁泄露)
46: aload_2
47: return
2
3
4
5
6
7
8
9
10
11
12
13
14
15
monitorenter:线程尝试获取锁,成功则进入临界区,失败则阻塞(进入Monitor的等待队列)。monitorexit:无论代码正常执行还是抛出异常,都会释放锁(保证锁一定会被释放)。
# 3.2.2 同步方法:ACC_SYNCHRONIZED标志
同步方法通过方法表中的ACC_SYNCHRONIZED标志实现,无需显式指令:
public class SyncMethodDemo {
public synchronized void test() { // 同步实例方法
System.out.println("同步方法代码");
}
}
2
3
4
5
反编译字节码核心片段:
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 同步方法标志
Code:
0: getstatic #2 // 执行方法逻辑
30: return
2
3
4
5
6
JVM执行时会检查ACC_SYNCHRONIZED标志:若存在,先获取锁(锁对象为this或Class),执行完方法后自动释放锁。
# 3.3 锁的底层结构:对象头与Monitor
synchronized的本质是对象锁——所有Java对象都可作为锁,因为每个对象内置了一个Monitor(监视器),且对象头存储了锁的状态信息。
# 3.3.1 对象的内存结构
Java对象在堆内存中分为三部分:
对象头(Header) | 实例数据(Instance Data) | 对齐填充(Padding)
其中对象头是实现锁的核心,包含:
- Mark Word:存储对象的锁状态、哈希码(HashCode)、GC年龄、线程ID等(动态变化)。
- Klass Pointer:指向对象所属类的元数据(如类的方法、字段信息)。
# 3.3.2 Mark Word的动态变化
Mark Word的内容会随锁状态改变,以32位JVM为例:
| 锁状态 | Mark Word存储内容(32位) | 锁标志位 | 偏向锁标志 |
|---|---|---|---|
| 无锁 | 哈希码(25位) + GC年龄(4位) + 0 + 01(未使用) | 01 | 0 |
| 偏向锁 | 线程ID(23位) + 偏向时间戳(2位) + GC年龄(4位) + 1 + 01 | 01 | 1 |
| 轻量级锁 | 指向栈中锁记录(Lock Record)的指针(30位) + 00 | 00 | - |
| 重量级锁 | 指向Monitor对象的指针(30位) + 10 | 10 | - |
| GC标记 | 空 + 11 | 11 | - |
# 3.3.3 Monitor的作用
当锁升级为重量级锁时,Mark Word会指向一个Monitor对象(C++结构体),其核心结构包括:
_owner:持有锁的线程(初始为null)。_WaitSet:等待锁的线程队列(调用wait()后进入)。_EntryList:竞争锁失败的线程队列(阻塞状态)。
线程获取锁时,_owner设为当前线程;释放锁时,_owner设为null,并唤醒_EntryList中的线程竞争锁。
# 3.4 锁的升级机制(JDK 1.6+)
为减少锁的性能开销,JDK 1.6引入锁升级机制:锁状态从"无锁"逐步升级为"偏向锁"→"轻量级锁"→"重量级锁",避免直接使用重量级锁(性能低)。
# 升级流程详解:
无锁状态
对象刚创建时,Mark Word存储哈希码和GC年龄,无锁标志(01),偏向锁标志(0)。偏向锁(单线程优化)
- 触发条件:只有一个线程竞争锁。
- 原理:线程第一次获取锁时,通过CAS将Mark Word的"线程ID"设为自己的ID。后续该线程进入/退出锁时,只需检查线程ID是否为自己,无需CAS操作(几乎无开销)。
- 撤销:若其他线程尝试获取锁,偏向锁会撤销为无锁或轻量级锁(有额外开销,因此适合单线程长期持有锁的场景)。
轻量级锁(多线程交替执行)
- 触发条件:多个线程交替竞争锁(非同时竞争)。
- 原理:线程获取锁时,在栈中创建Lock Record(存储Mark Word副本),通过CAS将Mark Word改为指向Lock Record的指针。成功则获取锁;失败则通过自旋(循环重试)获取锁(避免线程阻塞,适合短任务)。
重量级锁(多线程同时竞争)
- 触发条件:自旋次数超过阈值(默认10次),或有线程阻塞。
- 原理:锁膨胀为重量级锁,Mark Word指向Monitor对象。竞争失败的线程进入Monitor的
_EntryList阻塞(不消耗CPU,但线程切换开销大)。
# 3.5 核心特性
- 原子性:通过Monitor保证同一时间只有一个线程进入临界区,确保代码块/方法的操作不可分割。
- 可见性:依赖内存屏障(Memory Barrier):
- 释放锁时,线程工作内存的变量会刷新到主内存;
- 获取锁时,线程工作内存会清空,从主内存重新读取变量。
- 有序性:禁止指令重排序(临界区内代码执行顺序稳定)。
- 可重入性:同一线程多次获取同一锁时,Mark Word记录线程ID,无需重新竞争(避免死锁)。
# 四、轻量级同步:Volatile
volatile是Java的轻量级同步关键字,用于修饰共享变量,解决可见性和有序性问题,但不保证原子性。
# 4.1 核心作用
- 可见性:一个线程修改
volatile变量后,其他线程能立即看到最新值(通过内存屏障强制刷新主内存)。 - 有序性:禁止指令重排序(通过内存屏障限制编译器和CPU的优化)。
# 4.2 为什么不保证原子性?
volatile仅能保证单个变量的读写可见,但无法保证复合操作(如i++)的原子性。例如:
public class VolatileAtomicDemo {
private static volatile int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> { for (int j = 0; j < 1000; j++) i++; });
Thread t2 = new Thread(() -> { for (int j = 0; j < 1000; j++) i++; });
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i); // 结果可能小于2000(原子性不保证)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 4.3 适用场景
- 状态标记位(如
boolean isRunning,控制线程启动/停止); - 单线程写、多线程读的变量(无需原子性)。
# 五、总结:线程锁的选择与实践
- synchronized:适合大多数场景,无需手动释放锁,JDK优化后性能接近
ReentrantLock。 - ReentrantLock:适合需要灵活控制(如公平锁、尝试获取锁、中断等待)的场景。
- volatile:适合轻量级同步(可见性/有序性),不涉及复合操作的场景。
核心原则:根据并发强度(竞争频率)、功能需求(公平性、可中断性)选择合适的锁,优先使用JDK内置机制(减少手动管理成本)。
# 扩展:常见问题
- 死锁如何产生?
多个线程相互持有对方需要的锁,且不释放(如线程1持有锁A等待锁B,线程2持有锁B等待锁A)。 - 如何避免死锁?
- 按固定顺序获取锁;
- 设定锁的获取超时时间(如
ReentrantLock.tryLock(timeout)); - 使用
LockSupport中断线程等待。