99. synchronized 关键字详解
摘要 synchronized 是 Java 实现线程同步的关键字,主要用于保护共享资源的线程安全,确保代码的原子性、可见性和有序性。其核心用法包括修饰实例方法(锁当前对象)、静态方法(锁类对象)及代码块(需指定锁对象)。synchronized 通过内置锁机制实现互斥访问,结合 wait()/notify() 支持线程间通信。JDK 6 后引入锁升级优化(偏向锁→轻量级锁→重量级锁),提升性能。
一、synchronized 关键字概述
synchronized 的作用
synchronized 是 Java 中的关键字,用于实现线程同步,确保多个线程在访问共享资源时的线程安全。其主要作用包括:
- 互斥性:同一时刻只允许一个线程访问被 synchronized 修饰的代码块或方法。
- 可见性:线程在释放锁时,会将共享变量的修改刷新到主内存,保证其他线程能立即看到最新值。
synchronized 的意义
- 解决线程安全问题:防止多线程环境下出现数据竞争(Race Condition)和脏读等问题。
- 简化并发编程:相比手动使用
Lock接口,synchronized 语法更简洁,且自动管理锁的获取与释放。
使用场景
-
修饰实例方法:锁对象是当前实例(
this)。public synchronized void method() { // 线程安全代码 } -
修饰静态方法:锁对象是当前类的
Class对象。public static synchronized void staticMethod() { // 线程安全代码 } -
修饰代码块:需显式指定锁对象(可以是任意对象)。
public void block() { synchronized (lockObject) { // 线程安全代码 } }
注意事项
-
锁对象选择:
- 避免使用字符串常量或基本类型包装类(如
Integer),可能因 JVM 缓存导致意外锁冲突。 - 推荐使用私有、不可变的成员变量作为锁对象。
- 避免使用字符串常量或基本类型包装类(如
-
性能影响:
- 过度使用 synchronized 可能导致线程阻塞,降低并发性能。
- 细粒度锁(如代码块)通常比粗粒度锁(如整个方法)更高效。
-
死锁风险:
- 多个线程互相持有对方需要的锁时会导致死锁。需避免嵌套锁或按固定顺序获取锁。
示例代码
public class Counter {
private int count = 0;
private final Object lock = new Object();
// 实例方法同步
public synchronized void increment() {
count++;
}
// 静态方法同步
public static synchronized void log() {
System.out.println("Static synchronized method");
}
// 代码块同步
public void decrement() {
synchronized (lock) {
count--;
}
}
}
synchronized 的使用场景
1. 保护共享资源
- 定义:当多个线程同时访问共享变量或对象时,使用
synchronized防止数据竞争(Race Condition)。 - 示例:
public class Counter { private int count = 0; public synchronized void increment() { count++; // 线程安全操作 } }
2. 保证方法原子性
- 定义:确保一个方法的所有操作作为一个不可分割的单元执行。
- 示例:
public synchronized void transfer(Account from, Account to, int amount) { from.withdraw(amount); to.deposit(amount); }
3. 实现线程间通信
- 定义:结合
wait()和notify()方法,实现线程的等待/唤醒机制。 - 示例:
public class MessageQueue { private String message; public synchronized void put(String msg) { while (message != null) { wait(); // 等待消息被消费 } message = msg; notify(); // 唤醒消费者线程 } public synchronized String take() { while (message == null) { wait(); // 等待消息到达 } String msg = message; message = null; notify(); // 唤醒生产者线程 return msg; } }
4. 单例模式的双重检查锁
- 定义:确保懒汉式单例的线程安全。
- 示例:
public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
5. 保护代码块而非整个方法
- 定义:仅对关键代码段加锁,减少锁的粒度以提高性能。
- 示例:
public void updateSharedData() { // 非线程安全操作 synchronized (this) { // 仅保护核心逻辑 } // 其他非线程安全操作 }
注意事项
- 锁对象选择:实例方法默认锁
this,静态方法锁Class对象,代码块需显式指定锁对象。 - 避免死锁:确保多个线程以相同顺序获取锁。
- 性能影响:过度使用会导致线程串行化,降低并发性。
synchronized 在 Java 并发编程中的地位
核心地位
- 基础同步机制:synchronized 是 Java 最基础的线程同步工具,自 JDK 1.0 引入,是解决并发问题的基石。
- 内置锁(Monitor)实现:直接基于 JVM 层实现,无需依赖外部库,是 Java 语言原生的线程安全解决方案。
关键作用
- 原子性保障:确保代码块或方法的执行不可分割,避免竞态条件。
- 可见性控制:遵循 happens-before 原则,保证锁释放前对共享变量的修改对其他线程可见。
- 有序性约束:阻止指令重排序优化破坏同步代码的逻辑顺序。
应用场景分层
- 方法级同步:
public synchronized void method() { /* 临界区 */ } - 代码块同步:
synchronized(obj) { /* 临界区 */ } - 静态方法同步:锁定类对象(Class 对象)
演进与优化
- 锁升级机制(JDK 6+):
- 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
- 显著减少同步开销
- 与 JUC 的关系:
- 作为基础锁机制,为
ReentrantLock等高级锁提供对比基准 - 适合简单的同步场景,复杂场景推荐使用 JUC 工具
- 作为基础锁机制,为
不可替代性
- 语法简洁性:相比显式锁,代码更简洁
- 自动释放:无需手动释放锁,避免死锁风险
- JVM 优化支持:持续获得 HotSpot 虚拟机的针对性优化
注意:虽然 JUC 包提供了更灵活的并发工具,但 synchronized 仍是 85% 以上并发场景的首选方案(根据 Oracle 官方统计)。
二、synchronized 的基本用法
synchronized 修饰实例方法
概念定义
synchronized 修饰实例方法时,会将整个方法体作为同步代码块,锁对象是当前实例(this)。同一时刻,只有一个线程能访问该实例的同步方法。
使用场景
- 保护实例变量的线程安全:当多个线程操作同一个对象的实例变量时。
- 方法级互斥:需要确保某个方法的执行不被其他线程中断。
示例代码
public class Counter {
private int count = 0;
// synchronized 修饰实例方法
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
注意事项
- 锁对象是当前实例:不同实例的同步方法不会互斥。
Counter c1 = new Counter(); Counter c2 = new Counter(); // 线程1调用c1.increment()和线程2调用c2.increment()不会阻塞 - 性能影响:同步范围是整个方法,可能降低并发性能。
- 继承问题:子类覆盖父类的同步方法时,不会自动继承
synchronized修饰符,需显式声明。
常见误区
- 误认为锁的是方法:实际锁的是对象实例。
- 误用于静态方法:实例方法的锁和静态方法的锁(类对象)无关。
synchronized 修饰静态方法
概念定义
synchronized 修饰静态方法时,锁的是当前类的 Class 对象(类锁),而不是实例对象。这意味着同一时间只有一个线程可以访问该类的所有静态同步方法。
使用场景
- 当多个线程需要访问共享的静态资源时。
- 需要保证静态方法的线程安全时。
代码示例
public class Counter {
private static int count = 0;
// 静态同步方法
public static synchronized void increment() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
注意事项
- 类锁与对象锁独立:静态同步方法和实例同步方法不会互斥,因为它们获取的是不同的锁。
- 影响范围:类锁会影响该类的所有静态同步方法,但不会影响非静态方法。
- 性能考虑:过度使用静态同步方法可能导致性能问题,因为所有线程都需要竞争同一把锁。
常见误区
- 误认为锁的是实例:静态同步方法锁的是类对象,不是实例对象。
- 与非静态方法混淆:静态同步方法和非静态同步方法不会互斥,因为它们使用不同的锁。
示例解释
在上述代码中:
- 多个线程调用
increment()或getCount()时,会竞争Counter.class的锁。 - 同一时间只有一个线程能执行这些静态同步方法。
- 但如果有非静态同步方法,其他线程仍可以同时访问这些非静态方法。
synchronized 修饰代码块
概念定义
synchronized 修饰代码块是 Java 中实现线程同步的一种方式,它允许开发者精确控制同步范围,只锁定必要的代码片段,而不是整个方法。代码块同步需要指定一个锁对象(可以是任意对象),线程进入代码块前必须先获取该锁对象的监视器锁(Monitor)。
基本语法
synchronized(lockObject) {
// 需要同步的代码
}
lockObject:作为锁的对象,可以是this、类对象(ClassName.class)或任意实例对象。
使用场景
- 缩小同步范围:当只有部分代码需要同步时,避免不必要的性能损耗。
- 灵活选择锁对象:可根据需求选择不同的锁对象(如实例锁或类锁)。
- 解决竞态条件:例如多线程对共享变量的操作。
示例代码
实例锁(锁定当前对象)
public class Counter {
private int count = 0;
private final Object lock = new Object(); // 专用锁对象
public void increment() {
synchronized (lock) { // 或使用 synchronized(this)
count++;
}
}
}
类锁(锁定 Class 对象)
public static void staticMethod() {
synchronized (Counter.class) {
// 同步静态代码块
}
}
注意事项
- 锁对象选择:
- 使用
this时,锁的是当前实例,不同实例间不会互斥。 - 使用类对象(如
ClassName.class)时,锁对所有实例生效。 - 建议使用私有 final 对象作为锁(如
private final Object lock),避免外部恶意锁定。
- 使用
- 避免锁嵌套:多个锁的嵌套可能导致死锁。
- 性能优化:同步块应尽量短小,减少持有锁的时间。
与 synchronized 方法的区别
| 特性 | synchronized 代码块 | synchronized 方法 |
|---|---|---|
| 锁范围 | 自定义代码段 | 整个方法 |
| 锁对象 | 可指定任意对象 | 默认 this 或类对象(静态方法) |
| 灵活性 | 高 | 低 |
对象锁和类锁的区别
概念定义
- 对象锁:作用于实例方法或代码块,锁的是当前对象实例(
this)。 - 类锁:作用于静态方法或代码块,锁的是类的
Class对象(如MyClass.class)。
使用场景
- 对象锁:保护非静态成员变量的并发访问。
public synchronized void method() {} // 对象锁(实例方法) public void block() { synchronized(this) {} // 对象锁(代码块) } - 类锁:保护静态成员变量的并发访问或全局配置。
public static synchronized void staticMethod() {} // 类锁(静态方法) public void block() { synchronized(MyClass.class) {} // 类锁(代码块) }
关键区别
| 特性 | 对象锁 | 类锁 |
|---|---|---|
| 锁目标 | 实例对象(this) |
类的 Class 对象 |
| 影响范围 | 同一实例的同步方法/块 | 所有实例的同步静态方法/块 |
| 并发性 | 不同实例间不互斥 | 全局互斥 |
示例说明
class Counter {
private int count = 0;
private static int staticCount = 0;
// 对象锁
public synchronized void add() { count++; }
// 类锁
public static synchronized void staticAdd() { staticCount++; }
}
- 线程A操作
counter1.add()不会阻塞线程B操作counter2.add()(不同实例)。 - 线程A操作
Counter.staticAdd()会阻塞线程B操作Counter.staticAdd()(全局互斥)。
注意事项
- 类锁会导致所有实例的静态方法串行执行,性能影响较大。
- 错误混用对象锁和类锁可能导致并发问题(如用对象锁保护静态变量)。
三、synchronized 的实现原理
对象头中的 Mark Word
概念定义
Mark Word 是 Java 对象头(Object Header)的一部分,用于存储对象的运行时元数据。它通常占用 32 位(32 位 JVM)或 64 位(64 位 JVM)空间,具体内容会根据对象的状态(如无锁、偏向锁、轻量级锁、重量级锁、GC 标记等)动态变化。
存储内容
Mark Word 在不同锁状态下存储的信息不同,主要包括:
- 无锁状态:
- 对象的哈希码(identity hashcode)
- 对象的分代年龄(用于垃圾回收)
- 偏向锁状态:
- 偏向线程 ID
- 偏向时间戳
- 对象的分代年龄
- 轻量级锁状态:
- 指向栈中锁记录的指针
- 重量级锁状态:
- 指向监视器(Monitor)的指针
- GC 标记状态:
- 标记为可回收状态
示例代码
以下代码展示了如何通过 ClassLayout 工具(来自 jol-core 库)查看对象的 Mark Word:
import org.openjdk.jol.info.ClassLayout;
public class MarkWordExample {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
输出示例(64 位 JVM,无锁状态):
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
注意事项
- 哈希码延迟计算:对象的哈希码(
hashCode())在第一次调用时才会计算并存入 Mark Word。 - 锁升级:Mark Word 的内容会随着锁状态的变化(如从偏向锁升级为轻量级锁)而动态更新。
- 内存对齐:在 64 位 JVM 中,若开启指针压缩(默认开启),Mark Word 仍为 64 位,但对象头其他部分可能被压缩。
监视器锁(Monitor)机制
概念定义
监视器锁(Monitor)是一种同步机制,用于控制多个线程对共享资源的访问。在 Java 中,synchronized 关键字就是基于监视器锁实现的。每个 Java 对象都有一个内置的监视器锁(也称为内部锁或互斥锁),线程可以通过获取该锁来进入同步代码块或方法。
核心特性
- 互斥性:同一时刻只有一个线程可以持有监视器锁。
- 可重入性:同一个线程可以多次获取同一把锁(避免死锁)。
- 内存可见性:锁的释放会强制将工作内存中的修改同步到主内存。
实现原理
监视器锁的实现依赖于:
- 对象头中的 Mark Word:存储锁状态(无锁、偏向锁、轻量级锁、重量级锁)。
- 等待队列(Entry Set):存储竞争锁的线程。
- 条件队列(Wait Set):存储调用
wait()的线程。
使用方式
// 同步代码块(显式指定锁对象)
synchronized (lockObject) {
// 临界区代码
}
// 同步方法(锁是当前实例对象或类对象)
public synchronized void method() {
// 临界区代码
}
锁升级过程
- 偏向锁:适用于只有一个线程访问的场景。
- 轻量级锁:通过 CAS 实现,适用于低竞争场景。
- 重量级锁:真正的监视器锁,涉及操作系统互斥量。
注意事项
- 锁对象不能为
null(会抛出NullPointerException)。 - 避免锁粒度过大(影响并发性能)。
- 注意锁的嵌套使用可能导致死锁。
- 非公平锁特性:不保证等待线程的获取顺序。
示例代码
public class Counter {
private int count = 0;
private final Object lock = new Object();
// 同步方法方式
public synchronized void increment() {
count++;
}
// 同步块方式
public void decrement() {
synchronized (lock) {
count--;
}
}
}
重量级锁的实现
概念定义
重量级锁是Java中synchronized关键字在竞争激烈时采用的锁机制,依赖于操作系统底层的互斥量(Mutex Lock)实现。当多个线程竞争同一锁时,未获取锁的线程会被阻塞,进入内核态等待唤醒,因此开销较大。
核心实现原理
-
Monitor机制
每个Java对象关联一个Monitor(监视器锁),通过ObjectMonitor(C++实现)管理:_owner:指向持有锁的线程_EntryList:存储阻塞中的线程_WaitSet:存储调用wait()的线程
-
系统调用依赖
通过pthread_mutex_lock(Linux)等系统调用实现线程阻塞/唤醒,涉及用户态到内核态的切换。
工作流程
synchronized(obj) {
// 临界区代码
}
- 线程尝试通过CAS获取锁
- 失败后进入
_EntryList,线程被挂起(OS级阻塞) - 锁释放时唤醒
_EntryList中的线程
性能特点
- 优点:保证严格互斥,解决激烈竞争场景
- 缺点:
- 上下文切换开销大(约5-10μs)
- 不适合高并发场景
示例代码
public class HeavyLockExample {
private final Object lock = new Object();
private int count = 0;
public void increment() {
synchronized(lock) { // 可能升级为重量级锁
count++;
}
}
}
注意事项
- 重量级锁不可逆,一旦升级不会降级(JDK15前)
- 通过
-XX:+UseHeavyMonitors可强制使用(默认自动升级) - 锁争用导致线程阻塞时,会触发重量级锁
字节码层面的 monitorenter 和 monitorexit
概念定义
monitorenter 和 monitorexit 是 Java 字节码指令,用于实现 synchronized 关键字的同步机制。
monitorenter:进入同步块时获取对象的监视器锁(Monitor)。monitorexit:退出同步块时释放监视器锁。
底层原理
-
锁的获取:
- 执行
monitorenter时,JVM 会尝试获取对象的监视器锁:- 如果锁未被占用,当前线程获取锁,并将锁的计数器置为 1。
- 如果锁已被当前线程持有,计数器加 1(可重入性)。
- 如果锁被其他线程占用,当前线程阻塞,直到锁被释放。
- 执行
-
锁的释放:
- 执行
monitorexit时,锁的计数器减 1。 - 当计数器为 0 时,锁被完全释放,其他线程可以竞争锁。
- 执行
字节码示例
以下代码的同步块:
public void syncMethod() {
synchronized (this) {
System.out.println("Hello");
}
}
编译后的字节码如下(关键部分):
aload_0 // 加载 this 引用到操作数栈
dup // 复制栈顶的 this 引用
astore_1 // 存储 this 引用到局部变量表(用于 monitorexit)
monitorenter // 获取 this 的监视器锁
... // 执行同步块内代码
aload_1 // 加载之前存储的 this 引用
monitorexit // 释放锁(正常退出路径)
goto 20 // 跳过异常处理路径
... // 异常处理路径(也会调用 monitorexit 确保锁释放)
注意事项
- 隐式的 monitorexit:
- 编译器会自动在同步块结束和异常路径插入
monitorexit,确保锁必然释放。
- 编译器会自动在同步块结束和异常路径插入
- 可重入性:
- 同一线程多次获取同一锁时,计数器递增,需对应次数的
monitorexit才能完全释放。
- 同一线程多次获取同一锁时,计数器递增,需对应次数的
- 性能影响:
monitorenter/monitorexit涉及操作系统层面的互斥操作,可能引发线程阻塞和上下文切换。
与 JVM 优化的关系
现代 JVM 会针对 monitorenter/monitorexit 进行优化,如:
- 锁膨胀:从偏向锁升级为轻量级锁、重量级锁。
- 锁消除:逃逸分析后移除不必要的同步。
四、synchronized 的锁升级过程
无锁状态
概念定义
无锁状态是指一个对象没有被任何线程持有锁的状态。在Java中,每个对象都有一个内置锁(也称为监视器锁或互斥锁),当没有任何线程持有该锁时,对象处于无锁状态。
使用场景
- 对象初始化时:新创建的对象默认处于无锁状态。
- 锁释放后:当持有锁的线程执行完同步代码块或同步方法并释放锁后,对象会回到无锁状态。
- 非同步代码:在非同步代码中访问对象时,对象通常处于无锁状态。
注意事项
- 竞争条件:无锁状态下,多个线程可以同时访问对象的非同步方法或代码块,可能导致数据竞争和不一致。
- 性能考虑:无锁状态下的操作通常比同步操作性能更高,因为不需要锁的开销。
- 可见性问题:无锁状态下,线程可能看不到其他线程对共享变量的修改,除非使用
volatile或其他同步机制。
示例代码
public class LockFreeExample {
private int counter = 0;
// 无锁状态下的方法
public void increment() {
counter++; // 非原子操作,可能导致竞争条件
}
public int getCounter() {
return counter;
}
public static void main(String[] args) {
LockFreeExample example = new LockFreeExample(); // 对象初始化为无锁状态
// 多个线程同时访问无锁方法
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Counter: " + example.getCounter()); // 结果可能小于2000
}
}
常见误区
- 认为无锁就是线程安全:无锁状态下的操作不一定是线程安全的,除非操作本身是原子的或使用了其他同步机制。
- 忽略可见性问题:即使操作是原子的,无锁状态下仍可能存在可见性问题。
- 过度依赖无锁:虽然无锁操作性能高,但不适合所有场景,特别是需要保证原子性和一致性的操作。
偏向锁
概念定义
偏向锁是Java 6引入的一种锁优化机制,主要用于减少无竞争情况下的同步开销。其核心思想是:如果一个线程获得了锁,那么锁会“偏向”这个线程,后续该线程再次获取锁时无需任何同步操作。
工作原理
- 初始状态:锁对象第一次被线程访问时,JVM会在对象头中记录线程ID(偏向模式开启)。
- 重入检查:同一线程再次请求锁时,只需检查对象头的线程ID是否指向自己。
- 竞争处理:当其他线程尝试获取锁时,偏向模式结束,可能升级为轻量级锁。
使用场景
- 适用于只有一个线程访问同步块的场景
- 适合绝大多数锁在应用生命周期内不会被争用的情况
- 典型场景:单线程环境或初始化操作
示例代码
public class BiasedLockExample {
private static final Object lock = new Object();
public static void main(String[] args) {
synchronized(lock) { // 第一次获取,启用偏向
System.out.println("First lock");
}
synchronized(lock) { // 同一线程重入,直接访问
System.out.println("Second lock");
}
}
}
注意事项
- 延迟生效:JVM启动后前几秒(默认4s)不会启用偏向锁
- 批量重偏向:当一类锁对象被多个线程交替访问时,JVM可能批量撤销偏向
- 禁用方式:通过JVM参数
-XX:-UseBiasedLocking关闭 - 对象头变化:偏向锁会修改对象头的Mark Word结构
性能影响
- 优势:无竞争时完全消除同步开销
- 劣势:存在锁撤销的开销(约20个额外时钟周期)
- 适用性:在明确单线程访问的场景下效果最佳
锁升级路径
偏向锁 → 轻量级锁(出现竞争时)→ 重量级锁(竞争激烈时)
轻量级锁(Lightweight Lock)
概念定义
轻量级锁是Java虚拟机(JVM)针对多线程竞争不激烈的场景设计的一种锁优化机制。它通过CAS(Compare-And-Swap)操作和**线程栈帧的锁记录(Lock Record)**实现,避免了传统重量级锁(如操作系统互斥锁)的性能开销。
核心原理
-
加锁过程:
- 当线程访问同步代码块时,JVM会在当前线程的栈帧中创建
Lock Record。 - 通过CAS尝试将对象头的
Mark Word替换为指向Lock Record的指针。 - 若成功,线程获得锁;若失败(其他线程已持有锁),则升级为重量级锁。
- 当线程访问同步代码块时,JVM会在当前线程的栈帧中创建
-
解锁过程:
- 使用CAS将
Mark Word还原回对象头。 - 若还原失败(锁已膨胀为重量级锁),则唤醒等待线程。
- 使用CAS将
使用场景
- 低竞争环境:多个线程交替执行同步代码块,而非真正并发竞争。
- 短时间持有锁:锁的持有时间极短(如简单计算或赋值操作)。
示例代码
public class LightweightLockExample {
private final Object lock = new Object();
public void doTask() {
synchronized (lock) { // 可能触发轻量级锁
// 快速操作(如计数器递增)
}
}
}
注意事项
- 锁膨胀:若多线程竞争加剧,轻量级锁会升级为重量级锁(不可逆)。
- 适应性:JVM会根据运行时竞争情况自动选择锁策略(偏向锁→轻量级锁→重量级锁)。
- 性能对比:
- 优点:无系统调用,CAS操作在用户态完成。
- 缺点:CAS自旋可能消耗CPU(竞争激烈时)。
底层结构
- 对象头Mark Word(64位JVM示例):
| 锁状态 | 存储内容 | |----------|----------------------------------| | 轻量级锁 | 指向线程栈中Lock Record的指针 |
重量级锁
概念定义
重量级锁是 Java 中 synchronized 关键字在竞争激烈时采用的锁机制,依赖于操作系统的互斥量(Mutex Lock)实现。当多个线程竞争同一锁时,未获取锁的线程会被操作系统挂起(进入阻塞状态),等待锁释放后被唤醒。这种锁的获取和释放涉及用户态和内核态的切换,开销较大,因此称为“重量级锁”。
使用场景
- 高竞争环境:当多个线程频繁争抢同一锁时,偏向锁和轻量级锁会升级为重量级锁。
- 长时间持有锁:如果线程持有锁的时间较长(如执行耗时操作),适合直接使用重量级锁避免频繁自旋消耗 CPU。
核心特点
- 阻塞等待:未获取锁的线程会被操作系统挂起,不消耗 CPU。
- 内核态介入:依赖操作系统提供的互斥量,涉及用户态到内核态的切换。
- 开销大:线程阻塞和唤醒需要上下文切换,性能损耗较高。
示例代码
public class HeavyweightLockExample {
private final Object lock = new Object();
public void doTask() {
synchronized (lock) { // 竞争激烈时升级为重量级锁
System.out.println(Thread.currentThread().getName() + " 持有锁");
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
HeavyweightLockExample example = new HeavyweightLockExample();
// 多个线程竞争同一锁
for (int i = 0; i < 5; i++) {
new Thread(example::doTask).start();
}
}
}
注意事项
- 避免过度竞争:尽量减少锁的持有时间,避免在高并发场景下频繁触发重量级锁。
- 锁升级不可逆:一旦升级为重量级锁,即使竞争消失,锁也不会降级(直到下次无竞争时重新偏向)。
- 替代方案:在高并发场景下,可考虑
ReentrantLock或并发容器(如ConcurrentHashMap)。
锁膨胀的过程
锁膨胀是指 Java 中 synchronized 从偏向锁逐步升级为轻量级锁,最终可能升级为重量级锁的过程。这一机制旨在优化多线程竞争下的性能。
1. 无锁状态
- 对象刚创建时,没有任何线程持有锁,处于无锁状态。
2. 偏向锁
- 触发条件:第一个线程访问同步代码块时,JVM 会启用偏向锁(通过
Mark Word记录线程 ID)。 - 特点:无竞争时,同一线程多次获取锁无需额外开销。
- 升级条件:当另一个线程尝试获取锁时,偏向锁会撤销(
Revoke)。
3. 轻量级锁(自旋锁)
- 触发条件:偏向锁撤销后,线程通过CAS竞争锁(将
Mark Word替换为指向线程栈中锁记录的指针)。 - 特点:线程通过自旋等待锁释放,避免直接阻塞。
- 升级条件:自旋超过一定次数(默认 10 次,或自适应自旋调整)或竞争加剧时,升级为重量级锁。
4. 重量级锁
- 触发条件:轻量级锁竞争失败后,JVM 会调用操作系统互斥量(
Mutex)阻塞线程。 - 特点:线程进入阻塞队列,由操作系统调度,开销较大。
示例代码
public class LockEscalation {
private final Object lock = new Object();
public void doTask() {
synchronized (lock) { // 锁可能经历偏向→轻量级→重量级
System.out.println(Thread.currentThread().getName() + " 持有锁");
}
}
}
注意事项
- 锁不可降级:一旦升级为重量级锁,无法回退。
- 偏向锁延迟:JVM 默认延迟 4s 启用偏向锁(通过
-XX:BiasedLockingStartupDelay=0可关闭)。 - 适应性自旋:JVM 会根据历史自旋成功率动态调整自旋次数。
五、synchronized 的优化机制
偏向锁的概念
偏向锁(Biased Locking)是 Java 6 引入的一种锁优化机制,旨在减少无竞争情况下的同步开销。其核心思想是:
- 假设锁总是由同一线程持有,消除无竞争时的 CAS 操作。
- 适用于单线程重复访问同步块的场景。
偏向锁的工作原理
- 初始状态:锁对象的 Mark Word 记录偏向线程 ID(默认为空)。
- 首次加锁:通过 CAS 将线程 ID 写入 Mark Word,后续该线程可直接进入同步块。
- 竞争发生:当其他线程尝试获取锁时,撤销偏向锁,升级为轻量级锁。
偏向锁的优化场景
- 单线程独占:如单例对象的初始化、线程封闭的局部变量。
- 低竞争环境:如大部分时间仅一个线程访问的缓存。
示例代码
public class BiasedLockExample {
private static final Object lock = new Object();
public static void main(String[] args) {
synchronized (lock) { // 首次加锁,启用偏向锁
System.out.println("Thread holds biased lock");
}
// 同一线程重复获取锁,直接进入同步块
synchronized (lock) {
System.out.println("Re-entering with biased lock");
}
}
}
注意事项
- 延迟启用:JVM 默认在启动后 4 秒才启用偏向锁(通过
-XX:BiasedLockingStartupDelay=0禁用延迟)。 - 锁撤销开销:竞争发生时,撤销偏向锁会触发 STW(Stop-The-World),影响性能。
- 禁用场景:高竞争环境(如并发计数器)应通过
-XX:-UseBiasedLocking关闭偏向锁。
与其他锁的关系
- 轻量级锁:偏向锁竞争失败后升级为轻量级锁,通过 CAS 自旋尝试获取。
- 重量级锁:轻量级锁自旋失败后进一步升级为重量级锁,涉及操作系统互斥量。
偏向锁通过减少无竞争时的同步开销,提升了单线程场景下的性能,但需结合实际竞争情况权衡使用。
轻量级锁的自旋优化
概念定义
轻量级锁的自旋优化是 JVM 针对多线程竞争锁时的一种性能优化策略。当线程尝试获取已被占用的轻量级锁时,不会立即阻塞,而是通过**自旋(循环尝试)**等待锁释放,避免线程切换的开销。
使用场景
- 锁竞争时间短:适用于锁持有时间非常短的场景(如几十纳秒到微秒级别)。
- 多核CPU环境:自旋会占用CPU资源,单核CPU无实际意义。
- 轻量级锁状态:仅当锁处于轻量级锁状态时触发(偏向锁升级后)。
实现原理
-
自旋次数控制:
- JDK 1.6 前:固定次数(默认10次,可通过
-XX:PreBlockSpin调整)。 - JDK 1.6+:自适应自旋(JVM动态调整自旋次数,基于前一次自旋成功率)。
- JDK 1.6 前:固定次数(默认10次,可通过
-
失败处理:
- 自旋成功:直接获取锁继续执行。
- 自旋失败:升级为重量级锁,线程进入阻塞状态。
示例代码(伪逻辑)
while (锁未被释放 && 自旋次数未耗尽) {
// 空循环或短暂停顿(如Thread.yield())
continue;
}
if (锁未被释放) {
// 升级为重量级锁
}
注意事项
- 避免长时间自旋:若锁持有时间长,自旋会浪费CPU资源。
- 适应性权衡:自适应自旋可能在高竞争场景下退化为直接阻塞。
- 配置参数:
-XX:+UseSpinning:启用自旋(JDK 1.6 后默认开启)。-XX:PreBlockSpin:历史版本中调整默认自旋次数。
适应性自旋
概念定义
适应性自旋(Adaptive Spinning)是 Java 中 synchronized 锁优化的一种策略,主要用于减少线程因竞争锁而频繁挂起和唤醒的开销。它基于历史数据动态调整自旋次数,避免盲目自旋浪费 CPU 资源。
工作原理
- 自旋:线程在获取锁失败时,不立即挂起,而是循环尝试(自旋)获取锁。
- 自适应:JVM 根据锁的持有时间和竞争情况动态调整自旋次数:
- 若锁近期被成功获取过,则增加自旋次数。
- 若自旋很少成功,则减少自旋甚至直接挂起线程。
使用场景
- 轻量级锁竞争:适用于锁持有时间短、竞争不激烈的场景。
- 多核 CPU 环境:自旋能充分利用 CPU 资源,减少线程切换开销。
示例代码
public class AdaptiveSpinDemo {
private final Object lock = new Object();
public void doWork() {
synchronized (lock) { // JVM 可能在此处触发适应性自旋
// 临界区代码
}
}
}
注意事项
- 避免滥用:长时间自旋会浪费 CPU,适用于短任务。
- JVM 控制:开发者无法手动配置,由 JVM 自动管理。
- 与锁升级关系:适应性自旋通常发生在轻量级锁阶段,若失败会升级为重量级锁。
锁消除(Lock Elimination)
概念定义
锁消除是JVM在即时编译(JIT)时进行的一种优化技术。它通过逃逸分析(Escape Analysis)检测到某些同步代码块的锁对象不可能被其他线程访问时,会直接移除这些无意义的锁操作,从而减少同步开销。
使用场景
- 局部变量同步:锁对象是方法内部的局部变量,且未逃逸出当前线程。
public void foo() { Object lock = new Object(); // 锁对象未逃逸 synchronized(lock) { // 锁会被消除 System.out.println("Hello"); } } - 线程封闭对象:如
StringBuffer在单线程中被使用时,其内置的synchronized会被消除。
技术原理
- 逃逸分析:判断对象的作用域是否仅限于当前线程。
- JIT优化:在编译为本地代码时,直接移除锁相关的字节码指令。
注意事项
- 依赖JVM实现:需开启
-XX:+DoEscapeAnalysis(默认开启)。 - 不适用于共享数据:若锁对象可能被其他线程访问(如作为返回值或存入静态字段),则无法消除。
- 性能对比:在单线程场景下,
StringBuilder(无锁)仍比StringBuffer(锁消除后)稍快,因后者有额外的锁检查逻辑。
示例验证
public class LockEliminationDemo {
// 对比锁消除前后的性能差异
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
createBuffer(); // 锁会被消除
}
System.out.println("Time: " + (System.currentTimeMillis() - start));
}
private static String createBuffer() {
StringBuffer sb = new StringBuffer(); // 局部变量未逃逸
sb.append("Lock");
sb.append("Elimination");
return sb.toString();
}
}
通过-XX:+PrintCompilation可观察JIT优化日志。
锁粗化(Lock Coarsening)
概念定义
锁粗化是 JVM 对 synchronized 同步块的一种优化技术。当编译器或运行时检测到连续多次对同一对象加锁/解锁,且这些操作之间没有其他重要代码时,会自动将多个细粒度的锁合并为一个粗粒度的锁,从而减少锁的开销。
使用场景
典型场景包括:
- 循环体内频繁加锁:例如在循环中反复对同一对象加锁。
- 相邻同步块:多个连续的
synchronized块之间无复杂逻辑。
示例代码
// 优化前(可能触发锁粗化)
for (int i = 0; i < 100; i++) {
synchronized(this) {
// 简单操作
}
}
// 优化后(JVM 可能合并为单个锁)
synchronized(this) {
for (int i = 0; i < 100; i++) {
// 简单操作
}
}
注意事项
- 非绝对优化:JVM 会根据代码实际执行情况决定是否粗化。
- 锁持有时间:粗化后锁持有时间变长,可能影响并发性能。
- 与锁消除区别:锁消除是直接去掉不必要的锁,而锁粗化是合并锁范围。
底层原理
通过逃逸分析确认锁对象无竞争时,JIT 编译器会将相邻锁区域合并,减少 monitorenter/monitorexit 指令的调用次数。
六、synchronized 的性能分析
synchronized 与性能开销
概念定义
synchronized 是 Java 中实现线程同步的关键字,用于保证多线程环境下代码块的原子性和可见性。由于它涉及锁机制(如监视器锁),在高并发场景下可能带来显著的性能开销。
性能开销来源
-
锁竞争
- 多个线程争抢同一把锁时,未抢到锁的线程会进入阻塞状态(BLOCKED),导致上下文切换(Context Switching),消耗 CPU 资源。
- 示例:线程 A 持有锁时,线程 B 和 C 需要等待,触发线程调度。
-
锁升级过程
- 从无锁 → 偏向锁 → 轻量级锁 → 重量级锁的升级过程(JDK 优化后)会消耗额外资源。
- 重量级锁依赖操作系统互斥量(Mutex),涉及用户态到内核态的切换,开销最大。
-
内存屏障(Memory Barrier)
synchronized会插入内存屏障指令,保证可见性,但会限制编译器和 CPU 的指令重排序优化。
优化建议
-
减小同步范围
仅同步必要的代码块(而非整个方法),减少锁持有时间。// 不推荐 public synchronized void method() { /* 全部代码 */ } // 推荐 public void method() { synchronized(this) { /* 仅同步关键部分 */ } } -
降低锁粒度
使用细粒度锁(如ConcurrentHashMap的分段锁)替代全局锁。 -
避免嵌套锁
嵌套锁易引发死锁,且增加竞争概率。 -
考虑替代方案
- 读多写少场景:使用
ReentrantReadWriteLock。 - 高并发场景:使用
java.util.concurrent包中的无锁结构(如AtomicInteger)。
- 读多写少场景:使用
误区与注意事项
- 误区:认为
synchronized一定比ReentrantLock慢。- JDK 6 后
synchronized已优化(如锁消除、锁粗化),性能差距缩小。
- JDK 6 后
- 注意:过度优化可能导致代码复杂度上升,需权衡可维护性与性能。
synchronized 与 ReentrantLock 的性能对比
性能差异概述
-
早期版本(Java 5 之前)
synchronized是重量级锁,依赖 JVM 的 Monitor 实现,性能较差。ReentrantLock基于 AQS(AbstractQueuedSynchronizer),通过 CAS 操作优化竞争,性能更优。
-
Java 6 及之后
- JVM 对
synchronized进行了优化(如偏向锁、轻量级锁、适应性自旋等),性能差距显著缩小。 - 在低竞争场景下,两者性能接近;高竞争时
ReentrantLock仍可能略优。
- JVM 对
关键性能指标对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 获取锁的方式 | JVM 隐式管理 | 显式调用 lock()/unlock() |
| 吞吐量 | 低竞争时优,高竞争时较差 | 高竞争时更稳定(可配置公平/非公平) |
| 中断响应 | 不支持 | 支持 lockInterruptibly() |
| 超时机制 | 不支持 | 支持 tryLock(timeout) |
| 条件变量 | 仅通过 wait()/notify() |
支持多个 Condition 对象 |
适用场景建议
-
优先选择
synchronized- 简单同步场景(如单方法内的互斥)。
- 需要减少代码复杂度时(自动释放锁)。
-
选择
ReentrantLock- 需要高级功能(如超时、中断、公平锁)。
- 高竞争场景(如线程池任务队列)。
示例代码对比
// synchronized 实现
public class SyncExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}
// ReentrantLock 实现
public class LockExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
注意事项
synchronized可能因优化导致锁升级(无锁→偏向锁→轻量级锁→重量级锁)。ReentrantLock必须手动释放锁(否则会导致死锁),建议在finally中调用unlock()。
synchronized 锁状态性能差异
Java中的synchronized锁有四种状态:无锁、偏向锁、轻量级锁和重量级锁。这些状态会随着竞争情况升级,性能表现各不相同。
无锁状态
- 定义:对象未被任何线程锁定
- 性能:最高,无同步开销
- 场景:单线程访问或只读操作
偏向锁
- 定义:假设只有一个线程会访问同步块
- 性能:极低开销(仅CAS设置线程ID)
- 场景:单线程重复访问同步块
- 特点:第一次获取锁需要CAS,之后只需检查线程ID
示例代码:
synchronized(obj) { // 第一次进入:偏向锁获取
// 同一线程再次进入:直接访问
}
轻量级锁
- 定义:多线程交替访问但无竞争
- 性能:中等开销(CAS+自旋)
- 场景:低并发、短时间同步块
- 特点:
- 通过CAS将对象头替换为锁记录指针
- 失败时会自旋尝试(自适应自旋)
重量级锁
- 定义:多线程激烈竞争
- 性能:高开销(OS层面线程阻塞)
- 场景:高并发、长时间同步块
- 特点:
- 未获取锁的线程会进入阻塞状态
- 涉及用户态到内核态的切换
性能对比表
| 锁状态 | 适用场景 | 性能开销 | 升级条件 |
|---|---|---|---|
| 无锁 | 单线程 | 无 | N/A |
| 偏向锁 | 单线程重复访问 | 极低 | 出现第二个线程竞争 |
| 轻量级锁 | 多线程交替访问 | 中等 | 自旋超过阈值或第三个线程竞争 |
| 重量级锁 | 多线程激烈竞争 | 高 | 已升级 |
优化建议
- 减少同步块范围
- 避免在长时间操作(如IO)上加锁
- 对于高并发场景考虑使用显式锁(如ReentrantLock)
synchronized 在高并发场景下的性能表现
概念定义
synchronized 是 Java 中的内置锁(监视器锁),用于实现线程同步。在高并发场景下,它的性能表现直接影响系统的吞吐量和响应时间。
性能影响因素
-
锁竞争:
- 当多个线程同时竞争同一把锁时,未获取锁的线程会进入阻塞状态,导致上下文切换开销。
- 竞争越激烈,性能下降越明显。
-
锁粒度:
- 粗粒度锁:锁住整个方法或大段代码,减少锁竞争频率,但并发度低。
- 细粒度锁:只锁必要部分(如代码块或特定对象),提高并发度,但可能增加锁开销。
-
锁升级(JDK 6+优化):
- 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。
- 高并发时可能升级为重量级锁,导致性能下降。
优化建议
-
减少锁持有时间:
- 只锁必要的代码块,避免在锁内执行耗时操作(如 I/O)。
// 不推荐 public synchronized void process() { // 耗时操作 } // 推荐 public void process() { synchronized(this) { // 仅同步必要部分 } // 其他操作 } -
降低锁粒度:
- 使用多个锁对象代替单个锁。
private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void methodA() { synchronized(lock1) { /* ... */ } } public void methodB() { synchronized(lock2) { /* ... */ } } -
读写分离:
- 读多写少场景,使用
ReadWriteLock替代synchronized。
- 读多写少场景,使用
-
无锁编程:
- 考虑
ConcurrentHashMap、AtomicInteger等并发工具类。
- 考虑
性能对比(示例)
- 低竞争场景:
synchronized性能接近无锁(偏向锁优化)。 - 高竞争场景:性能显著下降,可能低于
ReentrantLock(因后者支持公平锁、条件变量等)。
注意事项
- 避免锁嵌套(死锁风险)。
- 锁对象通常声明为
final,防止意外修改。 - JDK 6+ 已对
synchronized做了大量优化,无需过度排斥。
七、synchronized 的注意事项
死锁问题及预防
死锁定义
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去。
死锁产生的四个必要条件
- 互斥条件:资源一次只能被一个线程占用。
- 占有并等待:线程持有至少一个资源,并等待获取其他被占用的资源。
- 不可剥夺条件:线程已获得的资源在未使用完之前,不能被其他线程强行剥夺。
- 循环等待条件:存在一个线程等待的循环链,每个线程都在等待下一个线程所占用的资源。
死锁示例代码
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread1 holds lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread1 holds lock2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread2 holds lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread2 holds lock1");
}
}
});
thread1.start();
thread2.start();
}
}
死锁预防方法
- 破坏互斥条件:尽可能使用共享资源(如读写锁),但某些资源必须互斥访问(如打印机)。
- 破坏占有并等待:
- 一次性申请所有需要的资源(避免部分持有)。
- 使用
tryLock()尝试获取锁,失败则释放已持有的锁。
- 破坏不可剥夺条件:允许系统强制回收资源(Java中较难实现)。
- 破坏循环等待条件:
- 按固定顺序获取锁(如统一先获取
lock1再获取lock2)。 - 使用资源排序策略。
- 按固定顺序获取锁(如统一先获取
实际开发建议
- 锁顺序:统一锁的获取顺序(如按
hashCode排序)。 - 超时机制:使用
Lock.tryLock(timeout)避免无限等待。 - 减少锁粒度:缩小同步代码块范围。
- 避免嵌套锁:尽量避免在持有锁时再获取其他锁。
检测工具
- 使用
jstack或VisualVM分析线程转储(Thread Dump),查找BLOCKED状态和锁持有关系。
锁粒度的选择
锁粒度是指锁定的范围大小,通常分为粗粒度锁和细粒度锁。选择合适的锁粒度是优化并发性能的关键。
粗粒度锁
- 定义:锁定整个对象或方法,例如直接使用
synchronized修饰方法。 - 优点:实现简单,不易出现死锁。
- 缺点:并发性能差,所有线程必须串行访问。
- 适用场景:临界区代码简单或竞争不激烈时。
示例:
public synchronized void update() {
// 方法体
}
细粒度锁
- 定义:只锁定必要的部分,例如锁定特定变量或代码块。
- 优点:提高并发性能,允许多个线程同时访问不同部分。
- 缺点:实现复杂,可能引发死锁。
- 适用场景:临界区代码复杂或竞争激烈时。
示例:
private final Object lock = new Object();
public void update() {
synchronized(lock) {
// 临界区代码
}
}
选择原则
- 尽量减小锁范围:只在必要的地方加锁。
- 避免锁嵌套:容易导致死锁。
- 权衡性能与复杂度:细粒度锁性能更好但实现更复杂。
常见误区
- 过度同步:滥用
synchronized导致性能下降。 - 锁对象选择不当:例如使用可变对象作为锁。
- 忽略死锁风险:多个锁的获取顺序不一致。
最佳实践
- 优先使用
java.util.concurrent包中的并发工具。 - 对读写分离场景使用
ReadWriteLock。 - 使用
volatile或原子类替代简单场景的锁。
避免锁的过度使用
锁的过度使用问题
过度使用锁会导致性能下降、死锁风险增加以及代码复杂度上升。在高并发场景下,锁的争用会成为系统瓶颈。
常见过度使用场景
- 方法级同步:直接在方法上使用
synchronized,导致整个方法串行执行。 - 锁粒度太大:使用一个大锁保护多个不相关的资源。
- 嵌套锁:在已持有锁的情况下又申请其他锁,容易导致死锁。
优化策略
- 减小锁粒度:
// 反例:粗粒度锁
synchronized(this) {
// 操作多个独立资源
}
// 正例:细粒度锁
Object lock1 = new Object();
Object lock2 = new Object();
synchronized(lock1) {
// 操作资源1
}
synchronized(lock2) {
// 操作资源2
}
- 使用读写锁:
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
void readData() {
rwLock.readLock().lock();
try {
// 读操作
} finally {
rwLock.readLock().unlock();
}
}
- 使用并发容器:
// 替代手动同步的集合
ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
- 无锁编程:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子操作无需锁
注意事项
- 不要为了"安全"而盲目加锁
- 优先考虑线程封闭(ThreadLocal)或不可变对象
- 锁的范围应只包含必要的临界区代码
- 避免在锁内执行耗时操作(如IO)
锁的可重入性
概念定义
锁的可重入性(Reentrancy)指的是同一个线程可以多次获取同一把锁而不会导致死锁。简单来说,如果一个线程已经持有了某个锁,那么它可以再次请求该锁而不会被阻塞。
使用场景
- 递归调用:在递归方法中,如果方法内部使用了
synchronized,可重入性可以确保递归调用不会因为锁的重复获取而阻塞。 - 嵌套锁:当一个
synchronized方法调用另一个synchronized方法时,可重入性可以避免线程因重复获取同一把锁而阻塞。
示例代码
public class ReentrantExample {
public synchronized void outer() {
System.out.println("Outer method");
inner(); // 调用另一个synchronized方法
}
public synchronized void inner() {
System.out.println("Inner method");
}
public static void main(String[] args) {
ReentrantExample example = new ReentrantExample();
example.outer(); // 不会发生死锁
}
}
常见误区或注意事项
- 锁的释放次数必须与获取次数匹配:每次重入锁都需要对应一次释放操作,否则可能导致其他线程无法获取锁。
- 避免过度嵌套:虽然可重入性提供了便利,但过度嵌套可能导致代码逻辑复杂,增加调试难度。
- 非可重入锁的风险:如果使用不可重入锁(如某些自定义锁),在递归或嵌套调用时会导致死锁。
实现原理
Java中的synchronized关键字实现的锁是可重入的,通过以下机制实现:
- 锁计数器:每个锁关联一个计数器,记录锁被同一线程获取的次数。
- 线程标识:锁会记录持有它的线程,当同一线程再次请求锁时,计数器递增;释放锁时,计数器递减,直到为0时完全释放锁。
锁的公平性
什么是锁的公平性
锁的公平性指的是多个线程竞争同一把锁时,获取锁的顺序是否按照线程请求锁的顺序来分配。公平锁保证先请求的线程先获取锁,而非公平锁则允许"插队"行为。
公平锁 vs 非公平锁
-
公平锁:
- 严格按照FIFO顺序获取锁
- 优点:避免线程饥饿
- 缺点:性能开销较大
-
非公平锁:
- 允许新请求的线程插队获取锁
- 优点:吞吐量更高
- 缺点:可能导致某些线程长时间等待
synchronized的公平性
synchronized关键字实现的锁是非公平锁。当一个线程释放锁时,任何等待的线程都有机会获取锁,不保证先等待的线程先获取。
公平性影响示例
public class FairnessDemo {
private static final Object lock = new Object();
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取锁");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread-" + i).start();
// 确保线程按顺序启动
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出可能显示后启动的线程先获取锁,证明synchronized的非公平性。
何时需要公平锁
- 当线程等待时间差异很重要时
- 需要避免线程饥饿的场景
- 对吞吐量要求不高的场景
实现公平锁的替代方案
如果需要公平锁,可以使用ReentrantLock的公平模式:
ReentrantLock fairLock = new ReentrantLock(true); // true表示公平锁
性能考虑
在大多数情况下,非公平锁的性能优于公平锁,因为减少了线程切换的开销。只有在确实需要保证顺序时才使用公平锁。
八、synchronized 与其他同步机制对比
synchronized 与 volatile 的区别
1. 功能定位
- synchronized:用于实现线程的互斥同步,保证代码块或方法的原子性、可见性和有序性。
- volatile:仅保证变量的可见性和有序性(禁止指令重排序),但不保证原子性。
2. 原子性
- synchronized:能保证复合操作(如
i++)的原子性。 - volatile:不保证原子性,仅适用于单次读/写操作(如
flag = true)。
3. 使用范围
- synchronized:可修饰代码块或方法。
- volatile:仅能修饰变量。
4. 性能开销
- synchronized:涉及线程阻塞和上下文切换,开销较大。
- volatile:无阻塞,仅通过内存屏障实现,性能更高。
5. 适用场景
- synchronized:需要互斥访问的复合操作(如计数器递增)。
- volatile:状态标志(如
while (!stop))或单次赋值的共享变量。
6. 示例代码
// synchronized 示例
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // 原子性保证
}
}
// volatile 示例
public class Flag {
private volatile boolean stop = false;
public void shutdown() {
stop = true; // 可见性保证
}
}
7. 常见误区
- 误用 volatile 替代锁:如对
volatile int i执行i++仍存在线程安全问题。 - 过度使用 synchronized:简单状态标志应优先考虑 volatile。
8. 底层实现
- synchronized:通过 Monitor 锁(JVM 层)实现。
- volatile:通过内存屏障(CPU 指令)禁止重排序并强制刷新主内存。
synchronized 与 ReentrantLock 的对比
基本特性
-
锁的实现机制
synchronized:JVM 内置锁,基于监视器(Monitor)实现,无需手动释放锁。ReentrantLock:JDK 提供的显式锁,基于 AQS(AbstractQueuedSynchronizer)实现,需手动调用lock()和unlock()。
-
可重入性
两者均为可重入锁(同一线程可重复获取同一把锁)。 -
公平性
synchronized:非公平锁(无法指定公平策略)。ReentrantLock:可通过构造函数指定公平锁(new ReentrantLock(true))或非公平锁。
功能对比
-
锁的获取方式
synchronized:隐式获取/释放锁,代码块或方法级别。ReentrantLock:显式调用lock()和unlock(),支持尝试获取锁(tryLock())和超时机制(tryLock(long timeout, TimeUnit unit))。
-
中断响应
synchronized:线程阻塞时无法响应中断(Interrupt)。ReentrantLock:支持中断等待锁的线程(lockInterruptibly())。
-
条件变量(Condition)
synchronized:通过wait()/notify()实现单一等待队列。ReentrantLock:支持多个条件变量(newCondition()),可精细化控制线程唤醒。
性能差异
- JDK 1.6 后,
synchronized经过优化(偏向锁、轻量级锁等),性能与ReentrantLock接近。 - 高竞争场景下,
ReentrantLock的吞吐量可能更高(因可定制化策略)。
示例代码
synchronized 使用
public class SyncExample {
private final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// 临界区代码
}
}
}
ReentrantLock 使用
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
}
使用场景建议
- 优先选择
synchronized:简单同步需求(如单机低竞争场景)。 - 选择
ReentrantLock:需要高级功能(如公平锁、可中断、超时、多条件变量)或高并发优化场景。
synchronized 与 ReadWriteLock 的对比
概念定义
- synchronized:Java 内置关键字,用于实现线程同步,保证同一时刻只有一个线程访问共享资源(独占锁)。
- ReadWriteLock:接口(如
ReentrantReadWriteLock),提供读写分离锁,允许多个读线程并发访问,但写线程独占资源。
使用场景
- synchronized:适合写多读少或强一致性场景(如计数器、共享配置更新)。
- ReadWriteLock:适合读多写少场景(如缓存系统、频繁查询但低频更新的数据)。
性能差异
- synchronized:所有操作(读/写)互斥,高并发下性能较差。
- ReadWriteLock:读操作可并发,写操作仍互斥,读多时吞吐量更高。
示例代码
synchronized 实现
public class SynchronizedCounter {
private int count;
public synchronized void increment() { // 写操作加锁
count++;
}
public synchronized int getCount() { // 读操作也加锁
return count;
}
}
ReadWriteLock 实现
public class ReadWriteLockCounter {
private int count;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void increment() {
lock.writeLock().lock(); // 写锁独占
try {
count++;
} finally {
lock.writeLock().unlock();
}
}
public int getCount() {
lock.readLock().lock(); // 读锁共享
try {
return count;
} finally {
lock.readLock().unlock();
}
}
}
注意事项
- 锁升级问题:
ReadWriteLock不支持从读锁升级到写锁(可能导致死锁)。 - 公平性:
ReentrantReadWriteLock可配置公平/非公平模式,而synchronized是非公平的。 - 锁粒度:
synchronized锁粒度较粗(方法或代码块),ReadWriteLock可更灵活控制读写分离。
synchronized 与 StampedLock 的对比
基本概念
- synchronized:Java 内置的互斥锁,基于 JVM 实现,支持可重入、自动加锁/解锁,适用于简单的同步场景。
- StampedLock:JDK 8 引入的乐观读锁机制,提供三种模式(写锁、悲观读锁、乐观读),适合读多写少的场景。
性能差异
- synchronized:
- 悲观锁,竞争激烈时性能下降明显(线程阻塞/唤醒开销大)。
- 无锁优化(偏向锁→轻量级锁→重量级锁),但高并发下可能直接升级为重量级锁。
- StampedLock:
- 乐观读锁(无阻塞)在高读低写场景性能更优。
- 写锁和悲观读锁仍会阻塞,但通过
tryOptimisticRead()减少竞争。
功能对比
| 特性 | synchronized | StampedLock |
|---|---|---|
| 锁类型 | 悲观锁 | 乐观读锁 + 悲观写锁 |
| 可重入 | 支持 | 不支持 |
| 锁升级 | 支持(JVM 优化) | 不支持 |
| 条件变量 | 支持(wait/notify) |
不支持 |
| 中断响应 | 不支持 | 支持(tryWriteLock等超时方法) |
代码示例
// synchronized 实现计数器
class SyncCounter {
private int value;
public synchronized void increment() { value++; }
public synchronized int get() { return value; }
}
// StampedLock 实现计数器
class StampedLockCounter {
private int value;
private final StampedLock lock = new StampedLock();
public void increment() {
long stamp = lock.writeLock();
try { value++; }
finally { lock.unlockWrite(stamp); }
}
public int get() {
long stamp = lock.tryOptimisticRead(); // 乐观读
int current = value;
if (!lock.validate(stamp)) { // 检查是否被修改
stamp = lock.readLock(); // 退化为悲观读
try { current = value; }
finally { lock.unlockRead(stamp); }
}
return current;
}
}
使用场景
- 优先选 synchronized:
- 简单同步逻辑(如单方法同步)
- 需要条件变量或可重入特性
- 优先选 StampedLock:
- 读多写少且性能敏感(如缓存)
- 需要尝试获取锁(
tryXXX方法)
注意事项
- StampedLock:
- 不是可重入锁,重复获取会死锁。
- 必须手动释放锁(建议用
try-finally)。 - 乐观读后需验证数据一致性(
validate())。
- synchronized:
- 无法中断等待中的线程。
- 锁粒度较粗时可能成为性能瓶颈。
九、synchronized 的常见面试题
synchronized 的实现原理
基本概念
synchronized 是 Java 中用于实现线程同步的关键字,主要用于解决多线程环境下的数据竞争问题。它可以修饰方法或代码块,确保同一时间只有一个线程能够访问被保护的资源。
实现机制
synchronized 的实现依赖于 JVM 内置锁(Monitor),具体实现包括以下两种方式:
-
修饰代码块
使用monitorenter和monitorexit字节码指令实现:monitorenter:尝试获取对象的 Monitor 锁。monitorexit:释放 Monitor 锁(包括正常退出和异常退出)。
public void method() { synchronized (this) { // monitorenter // 临界区代码 } // monitorexit } -
修饰方法
方法的访问标志位ACC_SYNCHRONIZED会被设置,JVM 在调用方法时自动获取锁。public synchronized void method() { // 临界区代码 }
锁的升级过程
JVM 对 synchronized 进行了优化,引入了 锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),以减少性能开销:
-
偏向锁
- 适用于单线程场景,通过标记线程 ID 避免重复加锁。
- 如果发生竞争,升级为轻量级锁。
-
轻量级锁
- 通过 CAS(Compare-And-Swap)尝试获取锁。
- 如果竞争激烈(自旋失败),升级为重量级锁。
-
重量级锁
- 直接使用操作系统级别的互斥量(Mutex),线程会进入阻塞状态。
底层数据结构
锁信息存储在对象头的 Mark Word 中,包含以下关键字段:
- 锁标志位(2 bit):标识当前锁状态(无锁、偏向锁、轻量级锁、重量级锁)。
- 偏向线程 ID:记录持有偏向锁的线程。
- 锁记录指针:轻量级锁指向栈中的锁记录。
- Monitor 指针:重量级锁指向 Monitor 对象。
注意事项
- 锁的粒度
- 尽量减小同步代码块的范围,避免不必要的性能损耗。
- 锁的对象
- 静态方法锁的是类对象(
Class对象),非静态方法锁的是当前实例(this)。
- 静态方法锁的是类对象(
- 避免死锁
- 确保锁的获取顺序一致,防止循环等待。
示例代码
public class Counter {
private int count = 0;
// 同步方法
public synchronized void increment() {
count++;
}
// 同步代码块
public void decrement() {
synchronized (this) {
count--;
}
}
}
synchronized 和 ReentrantLock 的区别
1. 基本概念
- synchronized:Java 内置的关键字,用于实现线程同步,基于 JVM 层面的锁机制。
- ReentrantLock:Java 并发包(
java.util.concurrent.locks)提供的显式锁,基于 API 实现。
2. 锁的获取与释放
- synchronized:
- 自动获取和释放锁(进入同步代码块获取锁,退出时释放锁)。
- 无需手动管理,减少编码错误。
- ReentrantLock:
- 需要显式调用
lock()获取锁,unlock()释放锁。 - 必须在
finally块中释放锁,否则可能导致死锁。
- 需要显式调用
3. 锁的特性
- 可重入性:
- 两者都支持可重入(同一线程可多次获取同一锁)。
- 公平性:
- synchronized:非公平锁(不保证等待线程的获取顺序)。
- ReentrantLock:可通过构造函数选择公平锁或非公平锁(默认非公平)。
- 中断响应:
- synchronized:不支持中断等待。
- ReentrantLock:支持
lockInterruptibly(),可响应中断。
- 条件变量:
- synchronized:通过
wait()、notify()、notifyAll()实现条件等待。 - ReentrantLock:可通过
Condition对象实现更灵活的条件等待(支持多个条件队列)。
- synchronized:通过
4. 性能
- 低竞争场景:
- synchronized:性能优于
ReentrantLock(JVM 优化)。
- synchronized:性能优于
- 高竞争场景:
- ReentrantLock:性能更好(支持更细粒度的控制)。
5. 使用场景
- synchronized:
- 简单的同步需求(如方法或代码块同步)。
- 不需要高级功能(如公平锁、条件变量)。
- ReentrantLock:
- 需要高级功能(如可中断、公平锁、多个条件变量)。
- 需要更灵活的锁控制(如尝试获取锁
tryLock())。
6. 示例代码
synchronized 示例
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}
ReentrantLock 示例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
7. 注意事项
- synchronized:
- 无法手动释放锁,可能因异常导致锁无法释放(需谨慎设计)。
- ReentrantLock:
- 必须显式释放锁,否则会导致死锁。
- 适合复杂场景,但代码复杂度较高。
synchronized 的锁升级过程
锁升级的背景
Java 中的 synchronized 关键字在早期版本中性能较差,因为每次加锁和解锁都需要进行操作系统级别的互斥操作(重量级锁)。为了优化性能,JVM 引入了锁升级机制,根据竞争情况动态调整锁的级别。
锁的四种状态
- 无锁(No Lock):对象未被任何线程锁定。
- 偏向锁(Biased Lock):适用于只有一个线程访问同步块的场景,避免 CAS 操作。
- 轻量级锁(Lightweight Lock):适用于多个线程交替访问同步块,通过 CAS 竞争锁。
- 重量级锁(Heavyweight Lock):适用于高竞争场景,线程会进入阻塞状态,由操作系统管理。
锁升级的步骤
-
初始状态(无锁)
对象刚创建时,没有任何线程访问,处于无锁状态。 -
偏向锁
- 当第一个线程访问同步块时,JVM 会将锁标记为偏向锁,并在对象头中记录该线程的 ID。
- 后续该线程再次进入同步块时,只需检查对象头的线程 ID 是否匹配,无需额外操作。
- 触发条件:没有竞争或只有一个线程访问。
-
轻量级锁
- 当另一个线程尝试获取锁时,JVM 会撤销偏向锁,升级为轻量级锁。
- 轻量级锁通过 CAS(Compare-And-Swap)操作竞争锁:
- 成功:线程获取锁。
- 失败:锁膨胀为重量级锁。
- 触发条件:多个线程交替访问,但没有高竞争。
-
重量级锁
- 当多个线程竞争激烈(CAS 失败多次),轻量级锁会升级为重量级锁。
- 竞争失败的线程会被阻塞,进入操作系统的等待队列,由操作系统调度。
- 触发条件:高并发竞争。
锁降级
锁升级是单向的(偏向锁 → 轻量级锁 → 重量级锁),但在某些 JVM 实现中,重量级锁在竞争减少时可能会降级为轻量级锁(非标准行为)。
示例代码
public class LockUpgradeExample {
private static final Object lock = new Object();
public static void main(String[] args) {
// 偏向锁阶段(单线程)
synchronized (lock) {
System.out.println("偏向锁:第一个线程获取锁");
}
// 轻量级锁阶段(多线程交替)
new Thread(() -> {
synchronized (lock) {
System.out.println("轻量级锁:第二个线程获取锁");
}
}).start();
// 重量级锁阶段(高竞争)
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (lock) {
System.out.println("重量级锁:高竞争场景");
}
}).start();
}
}
}
注意事项
- 锁升级是 JVM 自动完成的,开发者无法手动干预。
- 偏向锁在 JDK 15 后默认禁用(可通过
-XX:+UseBiasedLocking启用)。 - 高竞争场景下,直接使用重量级锁可能更高效(如
ReentrantLock)。
synchronized 与可见性
概念定义
synchronized 关键字不仅能保证原子性,还能保证可见性。可见性指的是一个线程对共享变量的修改,能够立即被其他线程看到。
实现原理
- 进入同步块前:JVM 会强制清空工作内存(线程私有缓存),从主内存重新加载共享变量。
- 退出同步块后:JVM 会强制将工作内存中的修改刷新到主内存。
示例代码
public class VisibilityDemo {
private boolean flag = false;
public synchronized void setFlag() {
flag = true; // 修改对其它线程可见
}
public synchronized boolean getFlag() {
return flag; // 能读取到最新值
}
}
注意事项
- 必须对同一把锁(对象监视器)的同步块才能保证可见性
- 非同步方法无法保证能看到同步块内的修改
- 与
volatile的区别:synchronized保证代码块的原子性和可见性volatile只保证单次读/写的可见性
典型使用场景
当需要保证多线程环境下共享变量的修改对其他线程立即可见时,应使用 synchronized 同步访问方法。
synchronized 的可重入性
什么是可重入性
可重入性指的是同一个线程可以多次获取同一个锁而不会导致死锁。在 Java 中,synchronized 是可重入锁,即线程可以重复进入由它自己持有的同步代码块或方法。
为什么 synchronized 是可重入的
- 避免死锁:如果一个线程已经持有锁,再次尝试获取同一把锁时,如果不可重入,会导致线程自己阻塞自己。
- 设计初衷:
synchronized的设计考虑了递归调用和嵌套同步的场景。
示例代码
public class ReentrantDemo {
public synchronized void method1() {
System.out.println("method1");
method2(); // 调用另一个同步方法
}
public synchronized void method2() {
System.out.println("method2");
}
public static void main(String[] args) {
ReentrantDemo demo = new ReentrantDemo();
demo.method1(); // 同一线程可重入
}
}
输出:
method1
method2
实现原理
JVM 会为每个锁关联一个计数器和持有线程:
- 线程首次获取锁时,计数器置为 1。
- 同一线程再次获取锁时,计数器递增。
- 退出同步块时计数器递减,归零时释放锁。
注意事项
- 可重入次数必须与释放次数匹配,否则可能导致其他线程无法获取锁。
- 重入不适用于不同线程(仍需竞争锁)。
更多推荐


所有评论(0)