目录
- 一、synchronized 关键字的基础认知
- 1.1 什么是 synchronized?
- 工作原理
- 锁的存储位置
- 1.2 synchronized 的核心作用
- 1.2.1 原子性保障
- 1.2.2 可见性保障
- 1.2.3 有序性保障
- 二、synchronized 的使用场景及语法详解
- 2.1 修饰实例方法
- 详细说明
- 典型应用场景
- 注意事项
- 锁范围示意图
- 2.2 修饰静态方法
- 详细说明
- 典型应用场景
- 执行特点
- 锁范围示意图
- 2.3 修饰代码块
- 详细说明
- 三种常见用法详解
- 典型应用场景:双重检查锁定单例模式
- 性能优化建议
- 锁选择策略
- 三、synchronized 的底层实现原理
- 3.1 对象头:锁的存储载体
- 3.2 锁的升级过程:从偏向锁到重量级锁
- 锁升级过程示例
- 四、synchronized 的性能优化技巧
- 4.1 减小锁粒度:拆分锁对象,降低竞争频率
- 核心原理
- 实现细节
- 高级应用场景
- 性能对比
- 4.2 避免锁竞争:减少持有锁的时间
- 优化原则
- 典型错误模式
- 最佳实践
- 性能影响
- 4.3 利用锁消除:避免不必要的加锁
- JVM优化机制
- 适用条件
- 验证方法
- 实际应用
- 4.4 合理使用锁粗化:减少锁的获取与释放次数
- 触发条件
- 实现方式
- 权衡考虑
- 典型应用
- 优化示例
- 五、synchronized 的常见问题与解决方案
- 5.1 死锁:线程间相互等待锁资源
- 问题描述
- 代码示例(死锁场景)
- 执行结果
- 死锁产生的必要条件
- 解决方案
- 5.2 错误的锁对象:导致同步失效
- 问题描述
- 常见错误场景1:使用可变对象作为锁对象
- 问题原因
- 解决方案
- 常见错误场景2:使用字符串常量池中的对象作为锁对象
- 问题原因
- 解决方案
- 5.3 锁竞争导致的性能瓶颈
- 问题描述
- 典型场景
- 问题分析
- 解决方案
- 六、synchronized 与 Lock 接口的对比
- 6.1 功能特性对比
- 6.2 代码示例对比
- 6.2.1 synchronized 实现同步
- 6.2.2 Lock 接口实现同步
- 6.3 适用场景选择
- 优先选择 synchronized 的场景:
- 选择 Lock 接口的场景:
- 七、常用 JVM 参数与调试工具
- 7.1 与 synchronized 相关的 JVM 参数
- 详细参数说明与应用场景
- 7.2 调试 synchronized 的工具
- 7.2.1 jstack 工具详解
- 7.2.2 jconsole 使用指南
- 7.2.3 VisualVM 高级功能
- 7.2.4 Arthas 诊断实战
一、synchronized 关键字的基础认知
1.1 什么是 synchronized?
synchronized 是 Java 中用于实现线程同步的关键字,它提供了内置的锁机制来协调多线程对共享资源的访问。在 Java 并发编程中,synchronized 是最基本、最常用的同步手段之一。
工作原理
synchronized 通过对象监视器(Monitor)机制实现同步。当线程进入 synchronized 代码块时:
- 首先尝试获取对象的监视器锁(Monitor Lock)
- 如果锁可用,线程获取锁并进入同步块
- 如果锁被其他线程持有,当前线程进入阻塞状态,直到锁被释放
- 线程执行完同步代码后自动释放锁
锁的存储位置
在 JVM 中,每个对象都有一个对象头(Object Header),其中包含两部分重要信息:
- Mark Word:存储对象的哈希码、GC 分代年龄等信息
- Klass Pointer:指向对象类型数据的指针
- 如果是数组对象,还会有数组长度信息
synchronized 使用的锁信息就存储在 Mark Word 中,包括:
- 锁状态标志位
- 指向锁记录的指针
- 指向重量级锁的指针
- 偏向线程 ID 等
1.2 synchronized 的核心作用
1.2.1 原子性保障
原子性是指一个操作不可中断,要么全部执行成功,要么全部不执行。synchronized 通过互斥锁机制保证被其修饰的代码块的原子性执行。
典型应用场景:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // 非原子操作变为原子操作
}
}
在这个例子中,count++ 操作实际上包含三个步骤:
- 读取 count 的值
- 将值加 1
- 写回新值 如果没有 synchronized,多线程环境下可能导致更新丢失。
1.2.2 可见性保障
可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到修改后的值。synchronized 通过以下机制保证可见性:
- 线程获取锁时:
- 清空工作内存(本地内存)中的变量副本
- 从主内存重新读取最新值
- 线程释放锁时:
- 将工作内存中修改过的变量刷新到主内存
- 确保其他线程能看到最新修改
这符合 JMM(Java 内存模型)的 happens-before 原则中的监视器锁规则:对一个锁的解锁 happens-before 于随后对这个锁的加锁。
1.2.3 有序性保障
有序性是指程序执行的顺序按照代码的先后顺序执行。synchronized 通过以下方式保证有序性:
- 禁止指令重排序:编译器不会对同步块内的指令进行重排序优化
- 建立内存屏障(Memory Barrier):确保同步块内的指令不会"逃逸"到同步块之外执行
示例:
public class Singleton {
private static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 保证new操作的原子性和有序性
}
return instance;
}
}
二、synchronized 的使用场景及语法详解
synchronized 是 Java 中最基本的同步机制,用于解决多线程环境下的线程安全问题。它主要有三种使用方式,每种方式对应不同的锁对象和同步范围,适用于不同的并发场景。
2.1 修饰实例方法
详细说明
当 synchronized 修饰实例方法时,锁对象是当前类的实例对象(即 this 对象)。这种同步方式适用于需要保护实例变量的场景,如:
- 银行账户的余额操作
- 购物车的商品数量修改
- 计数器对象的自增操作
典型应用场景
假设我们有一个银行账户类,需要保证多线程环境下存款操作的原子性:
public class BankAccount {
private double balance;
// 使用synchronized修饰实例方法保证线程安全
public synchronized void deposit(double amount) {
double newBalance = balance + amount;
try {
// 模拟耗时操作
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance = newBalance;
}
public synchronized double getBalance() {
return balance;
}
}
注意事项
- 这种同步方式只对同一个实例对象的方法调用有效
- 不同实例对象的方法调用不会互斥
- 方法执行期间会阻塞其他线程调用同一实例的同步方法
- 应避免在同步方法中执行长时间操作,以免影响性能
锁范围示意图
[实例对象A] ├── 同步方法1 (锁A) └── 同步方法2 (锁A) [实例对象B] ├── 同步方法1 (锁B) └── 同步方法2 (锁B)
2.2 修饰静态方法
详细说明
当 synchronized 修饰静态方法时,锁对象是当前类的 Class 对象。这种同步方式适用于:
- 静态变量的操作
- 类级别的共享资源访问
- 单例模式的实现
典型应用场景
例如,我们需要一个全局计数器来统计所有实例的创建次数:
public class InstanceCounter {
private static int count = 0;
// 静态同步方法保证类变量的线程安全
public static synchronized void increment() {
count++;
}
public static synchronized int getCount() {
return count;
}
public InstanceCounter() {
increment();
}
}
执行特点
- 无论创建多少个实例对象,静态同步方法都会互斥执行
- 类加载时就会初始化 Class 对象,全局唯一
- 静态同步方法和实例同步方法使用不同的锁,不会互相阻塞
锁范围示意图
[Class对象] └── 静态同步方法 (锁Class) [实例对象A] [实例对象B] ...
2.3 修饰代码块
详细说明
synchronized 代码块是最灵活的同步方式,可以显式指定锁对象,适用于:
- 只需要同步部分代码而非整个方法
- 需要细粒度控制同步范围
- 使用非this对象作为锁的情况
三种常见用法详解
1. 使用 this 作为锁对象
public void DOSomething() {
// 非同步代码
System.out.println("非同步代码");
// 同步代码块
synchronized (this) {
// 需要同步的代码
System.out.println("同步代码块");
}
}
特点:等同于同步实例方法,但可以只同步部分代码
2. 使用 Class 对象作为锁对象
public void doSomething() {
synchronized (MyClass.class) {
// 需要同步的静态资源操作
System.out.println("同步静态资源");
}
}
特点:等同于静态同步方法,但可以只在需要的地方加锁
3. 使用自定义对象作为锁对象
private final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// 需要同步的代码
System.out.println("使用自定义锁");
}
}
优势:
- 可以创建多个锁对象实现更细粒度的控制
- 避免直接使用this或Class对象可能导致的问题
- 锁对象通常声明为final防止意外修改
典型应用场景:双重检查锁定单例模式
public class Singleton {
private static volatile Singleton instance;
private static final Object lock = new Object();
public static Singleton getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
性能优化建议
- 尽量减小同步代码块的范围
- 避免在同步块中调用耗时操作(如IO)
- 对于读多写少的场景,考虑使用读写锁
- 不同功能使用不同锁对象,减少锁竞争
锁选择策略
| 锁类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 实例锁 | 保护实例变量 | 简单直接 | 粒度较粗 |
| 类锁 | 保护静态变量 | 全局控制 | 可能影响性能 |
| 代码块锁 | 需要细粒度控制 | 灵活高效 | 实现较复杂 |
三、synchronized 的底层实现原理
要深入理解 synchronized 的工作机制,需要从 JVM 底层实现进行分析。在 JDK 1.6 之前,synchronized 被称为"重量级锁",因为它的实现完全依赖于操作系统的互斥量(Mutex)机制,每次线程的阻塞和唤醒都需要在用户态和内核态之间切换,带来较大的性能开销。根据测试数据,这种上下文切换的开销可能达到几十微秒级别,对于高并发场景影响显著。
JDK 1.6 及之后的版本中,HotSpot 虚拟机团队对 synchronized 进行了重大优化,引入了全新的锁升级机制,包括偏向锁、轻量级锁等概念,使其性能得到极大提升。在大多数低竞争场景下,性能表现甚至可以与 java.util.concurrent 包中的显式锁相媲美。
3.1 对象头:锁的存储载体
在 Java 对象的内存布局中,每个对象都包含以下三部分:
- 对象头(Object Header):存储对象运行时数据
- 实例数据(Instance Data):存储对象的实际字段信息
- 对齐填充(Padding):为了字节对齐而存在的填充数据
以 32 位 JVM 为例,普通对象的对象头结构如下表所示:
| 长度(bit) | 内容 | 详细说明 |
|---|---|---|
| 25 | 哈希码(HashCode) | 对象的哈希值,用于哈希表等数据结构 |
| 4 | GC 分代年龄 | 记录对象被 GC 标记的次数,达到阈值(默认15)会被移入老年代 |
| 1 | 是否偏向锁(biased_lock) | 0表示非偏向锁,1表示偏向锁 |
| 2 | 锁状态标志(lock) | 01:无锁;00:轻量级锁;10:重量级锁;11:GC标记 |
| 2 | (数组对象)数组长度 | 仅数组对象拥有,记录数组长度 |
当对象作为 synchronized 的锁对象时,JVM 主要通过修改对象头中的"锁状态标志"和"是否偏向锁"字段来记录锁状态。这里需要注意,在 64 位 JVM 中,对象头的结构会有所不同,但基本原理相同。
3.2 锁的升级过程:从偏向锁到重量级锁
JDK 1.6 引入的锁升级机制是 synchronized 性能优化的核心,其升级路径为: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 这个过程是单向的,一旦升级到重量级锁就不会再降级。
3.2.1 无锁状态(Lock-Free)
初始状态下,对象处于无锁状态:
- 锁状态标志:01
- 是否偏向锁:0
此时任何线程都可以通过 CAS 操作尝试获取锁。
3.2.2 偏向锁(Biased Locking)
设计背景:统计表明,大多数情况下锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。例如 GUI 应用中的事件分发线程、消息队列的消费线程等。
工作流程:
- 首次获取锁时,JVM 通过 CAS 操作将:
- 是否偏向锁置为1
- 锁状态标志保持01
- 将当前线程ID写入对象头
- 之后同一线程再次获取锁时,只需简单检查线程ID即可,无需任何同步操作
- 当其他线程尝试获取锁时,偏向模式立即结束,可能升级为轻量级锁
性能优势:完全避免了同步操作,获取锁的代价降低到只需一次内存访问。
3.2.3 轻量级锁(Lightweight Locking)
适用场景:多个线程交替执行同步块,但基本不会同时竞争锁的情况。例如生产者-消费者模式中生产者和消费者线程的交替执行。
详细工作流程:
- 在栈帧中创建锁记录(Lock Record),包含:
- Displaced Mark Word:对象头原始内容的拷贝
- 指向锁对象的指针
- 通过 CAS 操作尝试将对象头的 Mark Word 替换为指向锁记录的指针
- 如果成功:
- 锁状态标志变为00
- 线程获得轻量级锁
- 如果失败:
- 检查是否当前线程已持有锁(重入情况)
- 否则开始自旋等待(自适应自旋)
- 自旋失败后升级为重量级锁
自旋优化:JVM 采用自适应自旋策略,会根据之前自旋的成功率动态调整自旋次数。
3.2.4 重量级锁(Heavyweight Locking)
核心组件:
- 监视器(Monitor):每个Java对象都关联一个Monitor
- 等待队列(Entry Set):存放等待获取锁的线程
- 等待池(Wait Set):存放调用了wait()的线程
工作流程:
- 当锁升级为重量级锁后:
- 锁状态标志变为10
- Mark Word 指向Monitor对象
- 线程竞争锁时:
- 成功则执行同步代码
- 失败则进入阻塞状态,加入等待队列
- 锁释放时:
- 唤醒等待队列中的线程
- 被唤醒线程需要重新竞争锁
性能特点:
- 线程阻塞和唤醒涉及操作系统内核操作
- 上下文切换开销较大(约5-10μs)
- 适合锁竞争激烈且持有时间长的场景
锁升级过程示例
假设有一个同步方法:
public synchronized void increment() {
count++;
}
- 初始调用(线程A):无锁 → 偏向锁,对象头记录线程A ID
- 线程A再次调用:直接通过偏向锁访问
- 线程B首次调用:撤销偏向锁 → 轻量级锁,线程B通过CAS获取锁
- 线程A和B交替调用:维持轻量级锁状态
- 线程A和B同时竞争:轻量级锁 → 重量级锁,线程阻塞,进入等待队列
这种渐进式的锁升级策略,使得 synchronized 在各种竞争程度下都能保持较好的性能表现。
四、synchronized 的性能优化技巧
4.1 减小锁粒度:拆分锁对象,降低竞争频率
核心原理
通过将一个大锁拆分为多个小锁,允许不同线程同时访问不同的资源分区,从而提升系统吞吐量。这种技术特别适用于读多写少的场景,或者在数据可以自然分片的场景中。
实现细节
- 分区策略:通常采用哈希算法将数据分配到不同的分区,确保数据分布均匀
- 锁数量:一般设置为2的幂次方,便于位运算优化
- 扩容处理:需要考虑动态扩容时的锁迁移问题
高级应用场景
- 分布式系统中的分片策略
- 数据库连接池的并发控制
- 多级缓存系统的同步机制
性能对比
| 锁策略 | 并发度 | 内存开销 | 实现复杂度 |
|---|---|---|---|
| 全局锁 | 低 | 小 | 简单 |
| 分段锁 | 高 | 中等 | 中等 |
| 细粒度锁 | 最高 | 大 | 复杂 |
4.2 避免锁竞争:减少持有锁的时间
优化原则
- 临界区最小化:只将真正需要同步的代码放入同步块
- 计算与同步分离:将复杂计算移到锁外执行
- 资源准备前置:在获取锁前完成所有准备工作
典型错误模式
// 错误示例:在锁内执行IO操作
synchronized(lock) {
readFile(); // 耗时IO
processData();
writeFile(); // 耗时IO
}
最佳实践
- 使用局部变量暂存计算结果
- 采用读写锁分离读写操作
- 对长时间操作实现可中断锁
性能影响
- 锁持有时间减少50%,吞吐量可提升2-3倍
- 对于高频交易系统,这种优化效果尤为明显
4.3 利用锁消除:避免不必要的加锁
JVM优化机制
- 逃逸分析:判断对象是否可能被其他线程访问
- 栈封闭:确认对象生命周期仅限于当前栈帧
- 同步消除:移除不必要的同步操作
适用条件
- 锁对象是方法局部变量
- 锁对象不会逃逸出当前线程
- 同步块内没有访问共享资源
验证方法
# 禁用锁消除进行对比测试 java -XX:-EliminateLocks LockEliminationDemo
实际应用
- 工具类方法中的临时同步
- 不可变对象处理
- 线程封闭场景
4.4 合理使用锁粗化:减少锁的获取与释放次数
触发条件
- 连续多次对同一锁的获取/释放
- 循环体内的同步操作
- 相邻的同步代码块
实现方式
- JVM自动优化:HotSpot虚拟机会自动检测并优化
- 手动合并:开发人员显式扩大同步范围
权衡考虑
| 因素 | 锁细化 | 锁粗化 |
|---|---|---|
| 并发度 | 高 | 低 |
| 锁开销 | 大 | 小 |
| 适用场景 | 竞争激烈 | 操作密集 |
典型应用
- 批量数据处理
- 事务性操作
- 流水线处理
优化示例
// 优化前:频繁加锁
for (Item item : items) {
synchronized(lock) {
process(item);
}
}
// 优化后:单次加锁
synchronized(lock) {
for (Item item : items) {
process(item);
}
}
注意:锁粗化可能会降低并发度,需根据实际场景权衡使用。
五、synchronized 的常见问题与解决方案
5.1 死锁:线程间相互等待锁资源
问题描述
死锁是指两个或多个线程在执行过程中,因争夺锁资源而相互等待的一种状态。例如,线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1,此时两个线程会一直相互等待,无法继续执行。
代码示例(死锁场景)
public class DeadLockDemo {
// 两个不同的锁对象
private final Object lock1 = new Object();
private final Object lock2 = new Object();
// 线程1执行的方法:先获取lock1,再获取lock2
public void method1() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 获取lock1,等待lock2");
try {
Thread.sleep(100); // 模拟耗时操作,增加死锁概率
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 获取lock2,执行method1");
}
}
}
// 线程2执行的方法:先获取lock2,再获取lock1
public void method2() {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 获取lock2,等待lock1");
try {
Thread.sleep(100); // 模拟耗时操作,增加死锁概率
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 获取lock1,执行method2");
}
}
}
public static void main(String[] args) {
DeadLockDemo demo = new DeadLockDemo();
// 线程1执行method1
new Thread(demo::method1, "线程1").start();
// 线程2执行method2
new Thread(demo::method2, "线程2").start();
}
}
执行结果
线程 1 会输出"获取 lock1,等待 lock2",线程 2 会输出"获取 lock2,等待 lock1",之后两个线程会一直阻塞,无法继续执行,形成死锁。
死锁产生的必要条件
- 互斥条件:锁资源只能被一个线程持有。
- 请求与保持条件:线程持有一个锁后,又请求其他锁,且不释放已持有的锁。
- 不可剥夺条件:线程持有的锁不能被其他线程强制剥夺,只能由线程自己释放。
- 循环等待条件:多个线程形成环形等待锁资源的关系(如线程 A 等线程 B 的锁,线程 B 等线程 A 的锁)。
解决方案
只要破坏死锁产生的任意一个必要条件,即可避免死锁。在实际开发中,常用的解决方案如下:
- 按固定顺序获取锁:确保所有线程都按照相同的顺序获取多个锁。例如,在上述示例中,让method2也先获取lock1,再获取lock2,即可避免循环等待条件。
// 优化后的method2:按固定顺序获取锁(先lock1,再lock2)
public void method2() {
synchronized (lock1) { // 与method1的锁获取顺序一致
System.out.println(Thread.currentThread().getName() + " 获取lock1,等待lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 获取lock2,执行method2");
}
}
}
- 使用tryLock()尝试获取锁:使用
java.util.concurrent.locks.Lock接口的tryLock(long timeout, TimeUnit unit)方法,在指定时间内尝试获取锁。如果超时未获取到锁,则释放已持有的锁,避免线程永久阻塞。 - 定时释放锁:在持有锁的线程中,设置超时机制,当超过指定时间仍未获取到其他锁时,主动释放已持有的锁。
5.2 错误的锁对象:导致同步失效
问题描述
在使用synchronized修饰代码块时,如果锁对象选择不当(如使用可变对象、字符串常量池中的对象等),可能会导致同步失效,无法保证线程安全。
常见错误场景1:使用可变对象作为锁对象
public class BadLockObjectDemo1 {
// 可变锁对象:value可能被pTXbVQfh修改
private String lock = "lock";
public void updateLockAndSync() {
// 先修改锁对象的值(导致锁对象引用改变)
lock = "newLock";
// 此时的锁对象是"newLock",而非最初的"lock"
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 执行同步代码");
}
}
public static void main(String[] args) {
BadLockObjectDemo1 demo = new BadLockObjectDemo1();
// 线程1和线程2可能使用不同的锁对象,导致同步失效
new Thread(demo::updateLockAndSync, "线程1").start();
new Thread(demo::updateLockAndSync, "线程2").start();
}
}
问题原因
锁对象lock是一个可变的字符串引用,当lock = "newLock"执行后,锁对象的引用发生了改变。此时,线程 1 和线程 2 可能使用不同的锁对象(线程 1 使用"lock",线程 2 使用"newLock"),导致同步代码块无法实现同步效果。
解决方案
使用不可变对象作为锁对象,如private final Object lock = new Object();。final关键字确保锁对象的引用不会被修改,从而保证同步的有效性。
常见错误场景2:使用字符串常量池中的对象作为锁对象
public class BadLockObjectDemo2 {
// 使用字符串常量作为锁对象(存在于字符串常量池中)
private final String lock = "LOCK";
public void syncWithStringLock() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 执行同步代码");
try {
Thread.sleep(1000);
pTXbVQfh } catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
BadLockObjectDemo2 demo1 = new BadLockObjectDemo2();
BadLockObjectDemo2 demo2 = new BadLockObjectDemo2();
// demo1和demo2的lock对象引用的是字符串常量池中的同一个"LOCK"对象
// 线程1和线程2会竞争同一把锁,导致本应独立的实例同步代码块相互阻塞
new Thread(() -> demo1.syncWithStringLock(), "线程1").start();
new Thread(() -> demo2.syncWithStringLock(), "线程2").start();
}
}
问题原因
在Java中,字符串常量会被存储在字符串常量池中,相同内容的字符串常量会指向同一个对象。上述代码中,demo1和demo2的lock变量都指向常量池中的同一个"LOCK"对象,导致两个不同实例的同步代码块共享同一把锁。当线程1执行demo1的syncWithStringLock方法时,会持有该锁,线程2执行demo2的同名方法时会被阻塞,违背了"不同实例的同步代码块应相互独立"的预期。
解决方案
避免使用字符串常量或基本类型包装类(如Integer,其缓存机制也会导致类似问题)作为锁对象,改用new Object()创建独立的锁对象,或使用当前实例(this)作为锁对象(确保不同实例的锁相互独立)。
5.3 锁竞争导致的性能瓶颈
问题描述
在高并发场景下,如果多个线程频繁竞争同一把synchronized锁,会导致大量线程阻塞和唤醒,引发用户态与内核态的切换,增加系统开销,最终导致程序性能下降,出现响应延迟、吞吐量降低等问题。
典型场景
在秒杀系统中,所有请求线程都需要竞争同一把锁来修改库存变量,此时锁竞争会非常激烈,成为系统的性能瓶颈。
问题分析
当锁竞争激烈时,synchronized的锁状态会升级为重量级锁,线程会进入内核态等待队列。每次线程的阻塞和唤醒都需要切换内核态,而内核态切换的开销远大于用户态操作,大量的内核态切换会严重消耗CPU资源,导致程序处理请求的效率降低。
解决方案
- 减少锁竞争频率:
- 例如,将秒杀库存按商品ID分段,每个分段使用独立的锁,避免所有线程竞争同一把锁。
- 通过减小锁粒度(如分段锁)
- 使用无锁数据结构(如
AtomicInteger)
- 使用乐观锁替代悲观锁:
synchronized是悲观锁,默认认为线程会存在激烈竞争,每次获取锁都会阻塞其他线程。- 乐观锁(如基于
CAS的Atomic类、版本号机制)默认认为线程竞争较少,通过重试机制实现同步,避免线程阻塞。
// 使用AtomicInteger(乐观锁)替代synchronized修改库存
public class SeckillService {
// 无锁的原子类,避免锁竞争
private AtomicInteger stock = new AtomicInteger(1000);
public boolean seckill() {
// CAS操作:如果当前库存大于0,就将库存减1
int currentStock;
do {
currentStock = stock.get();
if (currentStock <= 0) {
return false;
}
} while (!stock.compareAndSet(currentStock, currentStock - 1));
return true;
}
}
- 读写分离:
- 对于读多写少的场景,使用
ReadwriteLock或StampedLock - 允许多个线程同时读取数据,但写操作需要独占锁
- 对于读多写少的场景,使用
- 锁粗化与锁消除:
- 锁粗化:将多个连续的锁操作合并为一个更大的锁操作
- 锁消除:JVM在编译时进行逃逸分析,消除不必要的锁
- 使用并发容器:
- 如
ConcurrentHashMap、CopyOnWriteArrayList等 - 这些容器内部已经实现了高效的并发控制机制
- 如
六、synchronized 与 Lock 接口的对比
在 Java 并发编程中,除了synchronized关键字,java.util.concurrent.locks.Lock接口也是常用的同步手段。Lock接口提供了比synchronized更灵活的同步控制能力,但两者在使用方式、功能特性和性能上存在差异。下面从多个维度对比两者的区别,帮助开发者根据业务场景选择合适的同步方式。
6.1 功能特性对比
synchronized 关键字
- 获取锁方式
- 隐式获取:进入同步代码块/方法时自动获取锁,退出时自动释放锁
- 示例:
synchronized(this) { ... }或public synchronized void method() { ... }
- 可中断性
- 不可中断:线程获取锁时会一直阻塞,无法被中断
- 场景:在死锁情况下无法通过中断恢复
- 超时获取锁
- 不支持:线程会一直阻塞,直到获取到锁
- 风险:可能导致线程永久阻塞
- 公平锁支持
- 非公平锁:默认情况下,线程获取锁的顺序不遵循请求顺序
- JVM优化:JDK 1.6后引入偏向锁、轻量级锁优化
- 条件变量支持
- 单一条件变量:通过
wait()、notify()、notifyAll()实现 - 限制:一个锁对象只能对应一个条件队列
- 单一条件变量:通过
Lock 接口(以 ReentrantLock 为例)
- 获取锁方式
- 显式获取:通过
lock()方法获取锁 - 必须通过
unlock()方法手动释放锁(通常在finally块中)
- 显式获取:通过
- 示例:
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
- 可中断性
- 可中断:支http://www.devze.com持
lockInterruptibly()方法 - 场景:可以响应
Thread.interrupt()中断等待
- 可中断:支http://www.devze.com持
- 超时获取锁
- 支持:通过
tryLock(long timeout, TimeUnit unit)方法 - 优势:避免线程永久阻塞,提高系统健壮性
- 支持:通过
- 公平锁支持
- 可配置:通过构造函数
ReentrantLock(boolean fair)配置 - 公平锁(
fair=true):遵循FIFO原则 - 非公平锁(
fair=false):默认配置,性能更好
- 可配置:通过构造函数
- 条件变量支持
- 多个条件变量:通过
newCondition()创建多个Condition对象 - 应用:可以实现更复杂的线程间协调
- 示例:
- 多个条件变量:通过
Condition notEmpty = lock.newCondition(); Condition notFull = lock.newCondition();
6.2 代码示例对比
6.2.1 synchronized 实现同步
public class SynchronizedExample {
private final Object lock = new Object(); // 锁对象
private int count = 0; // 共享变量
// 隐式获取和释放锁
public void increment() {
synchronized (lock) { // 同步代码块
count++; // 线程安全操作
}
}
public int getCount() {
synchronized (lock) { // 同步代码块
return count; // 线程安全读取
}
}
// 同步方法示例
public synchronized void syncMethod() {
// 方法体自动同步
}
}
6.2.2 Lock 接口实现同步
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private final Lock lock = new ReentrantLock(); // 显式创建锁对象
private int count = 0; // 共享变量
public void increment() {
// 显式获取锁
lock.lock(); // 可能阻塞
try {
// 业务逻辑:修改共享变量
count++;
} finally {
// 必须在finally中释放锁,避免异常导致锁泄漏
lock.unlock();
}
}
// 带超时的尝试获取锁
public boolean tryIncrement(long timeout, TimeUnit unit)
throws InterruptedException {
if (lock.tryLock(timeout, unit)) { // 尝试获取锁
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false; // 获取锁失败
}
public int getCount() {
www.devze.com lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
6.3 适用场景选择
优先选择 synchronized 的场景:
- 简单同步需求
- 单一线程安全的方法或代码块
- 不需要复杂的同步控制(如中断、超时)
- 代码可维护性
- 希望代码简洁易读
- 减少手动管理锁的风险(如锁泄漏)
- 性能考虑
- 低并发或中等并发场景
- JVM对synchronized的优化(偏向锁、轻量级锁)能带来较好性能
- 快速开发
- 原型开发或对性能要求不高的场景
选择 Lock 接口的场景:
- 高级同步需求
- 需要中断正在等待锁的线程
- 示例:处理超时任务时中断等待
- 超时控制
- 需要设置超时时间,避免线程永久阻塞
- 应用场景:网络请求超时控制
- 公平性要求
- 需要公平锁,确保线程按请求顺序获取锁
- 应用场景:对资源分配顺序有严格要求的系统
- 复杂条件控制
- 需要多个条件变量实现精细控制
- 典型应用:生产者-消费者模型中分别唤醒生产者和消费者
- 性能关键场景
- 高并发场景下需要更细粒度的控制
- 需要尝试获取锁而不阻塞(tryLock)
- 锁的可扩展性
- 未来可能需要扩展锁的功能
- Lock接口提供了更多扩展可能性
七、常用 JVM 参数与调试工具
7.1 与 synchronized 相关的 JVM 参数
详细参数说明与应用场景
| JVM 参数 | 作用 | 默认值 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| -XX:+UseBiasedLocking | 开启偏向锁优化 | JDK 1.6~1.14 默认开启(JDK 15 后废弃偏向锁) | 适用于单线程重复访问同步块的场景 | JDK 15+ 已废弃,在竞争激烈场景下可能反而降低性能 |
| -XX:-UseBiasedLocking | 关闭偏向锁优化 | - | 多线程竞争激烈场景 | 可减少锁升级开销,适用于高并发环境 |
| -XX:BiasedLockingStartupDelay | 偏向锁启动延迟时间(毫秒) | 4000ms(JDK 1.6 及之后) | 需要延迟启用偏向锁的特殊场景 | 设置0表示立即启用,常用于性能测试 |
| -XX:-EliminateLocks | 关闭锁消除优化 | 默认开启 | 调试锁行为 | 会禁用JVM的逃逸分析优化,影响性能 |
| -XX:-EliminateAllocations | 关闭标量替换优化(影响锁消除) | 默认开启 | 调试逃逸分析相关行为 | 关闭后会影响锁消除效果 |
| -XX:PreblockSpin | 轻量级锁自旋次数 | JDK 1.6 后由 JVM 动态调整,无需手动设置 | 历史版本调优 | 现代JVM已实现自适应自旋,不建议手动设置 |
典型应用场景示例:
- 对于Web服务应用,建议在JDK 15以下版本使用
-XX:+UseBiasedLocking以获得更好的单线程性能 - 在高并发交易系统中,可以尝试
-XX:-UseBiasedLocking来避免偏向锁的撤销开销 - 调试死锁问题时,可以临时关闭锁消除
-XX:-EliminateLocks来观察真实锁竞争情况
7.2 调试 synchronized 的工具
7.2.1 jstack 工具详解
功能:用于打印Java进程的线程堆栈信息,可查看线程的锁持有情况和等待情况,帮助定位死锁。
使用方式:
- 获取Java进程ID:
jps -l - 生成线程dump:
jstack -l <pid> > thread_dump.log - 分析dump文件中的锁信息:
- 查找
BLOCKED状态的线程 - 查看
waiting to lock <0x0000000712345678>信息 - 对应查找
locked <0x0000000712345678>的线程
- 查找
示例输出分析:
"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f8a3418b000 nid=0x1a3e waiting for monitor entry [0x00007f8a2bdfe000]
java.lang.Thread.State: BLOCKED (on object monitor) at com.example.DeadLock.run(DeadLock.java:20) - waiting to lock &l编程t;0x0000000712345678> (a java.lang.Object) - locked <0x0000000712345690> (a java.lang.Object)"Thread-2" #13 prio=5 os_prio=0 tid=0x00007f8a3418d800 nid=0x1a3f waiting for monitor entry [0x00007f8a2bcfe000]
java.lang.Thread.State: BLOCKED (on object monitor) at com.example.DeadLock.run(DeadLock.java:20) - waiting to lock <0x0000000712345690> (a java.lang.Object) - locked <0x0000000712345678> (a java.lang.Object)
7.2.2 jconsole 使用指南
功能特点:
- 图形化界面,直观易用
- 实时监控线程状态
- 可视化展示锁竞争情况
操作步骤:
- 启动jconsole:
jconsole - 连接目标Java进程
- 切换到"线程"选项卡
- 点击"同步监视器"按钮查看锁信息
- 双击线程可查看详细堆栈
适用场景:
- 开发环境实时监控
- 演示和教学场景
- 快速验证锁行为
7.2.3 VisualVM 高级功能
核心功能:
- 线程分析:
- 实时线程状态监控
- 线程dump生成与分析
- 死锁自动检测
- 性能分析:
- CPU和内存分析
- 方法级热点分析
- 锁竞争统计
使用流程:
graph TD
A[启动VisualVM] --> B[连接目标JVM]
B --> C[选择"线程"标签]
C --> D[查看线程状态图]
D --> E[生成线程dump]
E --> F[分析锁依赖关系]
插件扩展:
- Threads Inspector:增强线程分析能力
- Deadlock Detector:自动死锁检测
- Visual GC:可视化GC分析
7.2.4 Arthas 诊断实战
- 常用命令详解:
thread命令家族:thread:列出所有线程thread -b:检测死锁(找出阻塞线程及其锁持有者)thread <id>:查看指定线程堆栈thread --state BLOCKED:过滤阻塞状态线程
monitor命令:- 监控方法级锁竞争:
monitor -c 5 com.example.Service methodName
- 监控方法级锁竞争:
- 输出字段说明:
timestamp:时间戳class:类名method:方法名total:调用次数success:成功次数fail:失败次数avg-rt:平均响应时间fail-rate:失败率
- 高级诊断流程:
# 1. 查看阻塞线程
thread -b
# 2. 监控可疑方法的锁竞争
monitor -c 5 com.example.Controller doPost
# 3. 观察特定锁对象的持有情况
watch java.lang.Object toString '{params[0], returnObj}' -x 2 -b -s com.example.LockHolder.monitor
典型应用场景:
- 生产环境快速诊断锁问题
- 无法重现的偶发死锁分析
- 性能瓶颈定位(锁竞争)
到此这篇关于深入理解 Java 中的 synchronized 关键字的文章就介绍到这了,更多相关Java synchronized 关键字内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
加载中,请稍侯......
精彩评论