当前位置: 首页 > news >正文

synchronized 深度剖析:从语法到锁升级的完整演进

在 Java 并发编程中,synchronized是最基础也最常用的同步机制。从 JDK 1.0 诞生时的重量级锁,到 JDK 6 引入的锁升级机制(偏向锁→轻量级锁→重量级锁),synchronized的性能不断优化,成为保障线程安全的核心工具。然而,很多开发者对其的理解仍停留在 “加锁关键字” 的表层,对底层实现和锁升级细节知之甚少。本文将从语法使用入手,逐步深入到 JVM 层面的锁机制,解析synchronized如何从低效走向高效,以及在不同场景下的最佳实践。

一、synchronized 的语法使用:锁的三种形态

synchronized的核心作用是实现临界区的互斥访问,即同一时间只有一个线程能执行被保护的代码块。它有三种使用形式,分别对应不同的锁对象。

1.1 修饰实例方法:锁为当前对象实例

当synchronized修饰实例方法时,锁的对象是调用该方法的对象实例。不同实例间的锁相互独立,同一实例的多个synchronized方法共享同一把锁。

public class SynchronizedDemo {// 锁对象为当前SynchronizedDemo实例public synchronized void instanceMethod() {// 临界区代码System.out.println("实例方法同步");}public static void main(String[] args) {SynchronizedDemo demo1 = new SynchronizedDemo();SynchronizedDemo demo2 = new SynchronizedDemo();// 线程1调用demo1的同步方法new Thread(demo1::instanceMethod).start();// 线程2调用demo2的同步方法(与线程1不互斥,因为锁对象不同)new Thread(demo2::instanceMethod).start();}
}

特点

  • 锁的粒度是对象实例,适合保护对象级别的共享资源(如实例变量);
  • 若多个线程操作同一个实例,会竞争同一把锁;操作不同实例则无竞争。

1.2 修饰静态方法:锁为类的 Class 对象

synchronized修饰静态方法时,锁的对象是当前类的 Class 对象(全局唯一)。无论创建多少个实例,所有线程调用该静态方法都会竞争同一把锁。

public class SynchronizedStaticDemo {// 锁对象为SynchronizedStaticDemo.classpublic static synchronized void staticMethod() {// 临界区代码System.out.println("静态方法同步");}public static void main(String[] args) {SynchronizedStaticDemo demo1 = new SynchronizedStaticDemo();SynchronizedStaticDemo demo2 = new SynchronizedStaticDemo();// 线程1和线程2竞争同一把锁(Class对象),会互斥执行new Thread(demo1::staticMethod).start();new Thread(demo2::staticMethod).start();}
}

特点

  • 锁的粒度是类级别,适合保护静态变量等全局共享资源;
  • 所有实例共享同一把锁,竞争强度高于实例方法锁。

1.3 修饰代码块:锁为指定对象

synchronized代码块通过显式指定锁对象,实现更灵活的同步控制。锁对象可以是任意 Java 对象(推荐使用专门的锁对象,如Object lock = new Object())。

public class SynchronizedBlockDemo {private final Object lock = new Object(); // 显式锁对象private int count = 0;public void increment() {// 锁对象为lock,保护count的修改synchronized (lock) {count++;}}public int getCount() {synchronized (lock) { // 与increment共享同一把锁return count;}}
}

特点

  • 锁的粒度可自定义,能减少锁竞争(如用不同锁保护不同资源);
  • 避免了修饰方法时的锁粒度过大问题,是实际开发中推荐的方式。

二、锁升级机制:从偏向锁到重量级锁的演进

JDK 6 之前,synchronized的实现依赖操作系统的互斥量(Mutex),每次加锁解锁都需要在用户态和内核态之间切换,性能开销巨大(因此被称为 “重量级锁”)。JDK 6 为优化其性能,引入了锁升级机制:根据竞争强度,自动从偏向锁升级为轻量级锁,最终升级为重量级锁,实现 “按需分配” 性能开销。

锁升级的核心依据是竞争程度

  • 无竞争:使用偏向锁(几乎无开销);
  • 轻度竞争(线程交替执行):使用轻量级锁(自旋等待,避免内核态切换);
  • 重度竞争(多线程同时争抢):使用重量级锁(依赖操作系统互斥量)。

锁升级是不可逆的(偏向锁→轻量级锁→重量级锁),一旦升级为重量级锁,就不会再降级。

2.1 偏向锁:无竞争场景的最优解

设计初衷:在多数情况下,锁不仅不存在多线程竞争,还会由同一线程多次获取。偏向锁通过 “偏向” 第一个获取锁的线程,消除无竞争场景下的锁开销。

2.1.1 实现原理
  • 加锁:当线程第一次获取锁时,JVM 会将对象头中的Mark Word标记为 “偏向模式”,并记录该线程的 ID。后续该线程再次获取锁时,只需检查 Mark Word 中的线程 ID 是否为当前线程,无需其他操作(几乎零开销)。
  • 解锁:偏向锁不会主动释放,只有当其他线程尝试获取锁时,持有偏向锁的线程才会释放锁(触发偏向锁撤销)。

对象头 Mark Word 在偏向锁状态的结构(64 位 JVM):

位信息

含义

0~1 位

锁状态标记(01 表示偏向锁)

2 位

偏向锁标志(1 表示处于偏向模式)

3~12 位

偏向线程 ID

13~17 位

epoch(偏向锁的时间戳,用于批量重偏向)

18~23 位

未使用

24~63 位

对象哈希码(无竞争时延迟计算,偏向锁释放时才生成)

2.1.2 适用场景
  • 单线程重复获取锁的场景(如单线程操作集合);
  • 几乎无竞争的环境(如线程私有的同步代码块)。

优势:除第一次获取锁时有轻微开销,后续获取锁几乎无需成本。

劣势:存在锁撤销的开销(当其他线程尝试获取锁时,需要暂停持有偏向锁的线程,检查其状态)。

2.2 轻量级锁:应对线程交替执行的场景

当有其他线程尝试获取偏向锁时,偏向锁会被撤销,升级为轻量级锁。轻量级锁适用于线程交替执行同步代码块的场景,通过自旋避免进入重量级锁。

2.2.1 实现原理
  • 加锁
  1. 线程获取锁时,先在栈帧中创建锁记录(Lock Record),存储对象头中 Mark Word 的副本(Displaced Mark Word);
  2. 通过 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针(表示当前线程持有轻量级锁);
  3. 若 CAS 成功,当前线程获取锁;若失败(说明存在竞争),则自旋重试(默认自旋次数为 10 次)。
  • 解锁
  1. 通过 CAS 操作将对象头的 Mark Word 恢复为 Displaced Mark Word;
  2. 若 CAS 成功,解锁完成;若失败(说明锁已升级为重量级锁),则唤醒等待队列中的线程。

对象头 Mark Word 在轻量级锁状态的结构

位信息

含义

0~1 位

锁状态标记(00 表示轻量级锁)

2 位及以上

指向栈中锁记录(Lock Record)的指针

2.2.2 适用场景
  • 线程交替执行同步代码块(如两个线程轮流获取锁);
  • 竞争持续时间短(自旋等待能在短时间内获取到锁)。

优势:避免了重量级锁的内核态切换开销,通过自旋在用户态解决竞争。

劣势:自旋会消耗 CPU 资源,若竞争激烈(自旋失败),会升级为重量级锁,反而增加开销。

2.3 重量级锁:多线程并发争抢的最终方案

当轻量级锁的自旋失败(超过最大自旋次数或已有线程自旋),锁会升级为重量级锁。重量级锁依赖操作系统的互斥量(Mutex) 实现,适用于多线程同时争抢锁的场景。

2.3.1 实现原理
  • 加锁:线程获取重量级锁时,若锁已被占用,当前线程会被阻塞并放入等待队列(由操作系统维护),进入内核态等待;
  • 解锁:持有锁的线程释放锁后,会唤醒等待队列中的一个或多个线程,使其重新竞争锁。

对象头 Mark Word 在重量级锁状态的结构

位信息

含义

0~1 位

锁状态标记(10 表示重量级锁)

2 位及以上

指向操作系统互斥量(Mutex)的指针

2.3.2 适用场景
  • 多线程同时竞争锁(如高并发场景下的资源争抢);
  • 同步代码块执行时间长(自旋等待得不偿失)。

优势:适合重度竞争场景,不会浪费 CPU 资源(线程阻塞时不消耗 CPU)。

劣势:线程阻塞和唤醒需要在用户态和内核态之间切换,开销巨大(约为轻量级锁的 10~100 倍)。

2.4 锁升级的完整流程示例

public class LockUpgradeDemo {private static final Object lock = new Object();public static void main(String[] args) {// 阶段1:单线程获取锁,使用偏向锁new Thread(() -> {synchronized (lock) {System.out.println("线程1获取锁(偏向锁)");try { Thread.sleep(100); } catch (InterruptedException e) {}}}).start();// 阶段2:线程1释放锁后,线程2尝试获取,偏向锁撤销,升级为轻量级锁new Thread(() -> {try { Thread.sleep(200); } catch (InterruptedException e) {} // 等待线程1释放synchronized (lock) {System.out.println("线程2获取锁(轻量级锁)");try { Thread.sleep(100); } catch (InterruptedException e) {}}}).start();// 阶段3:线程2未释放时,线程3尝试获取,轻量级锁升级为重量级锁new Thread(() -> {try { Thread.sleep(250); } catch (InterruptedException e) {} // 线程2持有锁时争抢synchronized (lock) {System.out.println("线程3获取锁(重量级锁)");}}).start();}
}

流程解析

  1. 线程 1 首次获取锁,lock对象头变为偏向锁状态,记录线程 1 的 ID;
  1. 线程 1 释放锁后,线程 2 尝试获取,JVM 撤销偏向锁,升级为轻量级锁,线程 2 通过 CAS 获取锁;
  1. 线程 2 持有锁时,线程 3 尝试获取,轻量级锁自旋失败,升级为重量级锁,线程 3 进入内核态等待;
  1. 线程 2 释放锁后,操作系统唤醒线程 3,线程 3 获取重量级锁。

三、synchronized 与其他锁的对比:如何选择?

在 Java 并发包中,ReentrantLock等锁机制也能实现同步功能。了解synchronized与它们的差异,才能在实际开发中做出合理选择。

特性

synchronized

ReentrantLock

锁实现

JVM 层面(C++ 实现)

API 层面(Java 代码实现)

锁升级

支持(偏向锁→轻量级锁→重量级锁)

不支持,始终是重量级锁(但可通过公平性设置优化)

可中断

不可中断(获取锁时会一直阻塞)

可中断(tryLock (long timeout, TimeUnit unit))

公平性

非公平锁(无法设置)

支持公平锁和非公平锁(构造函数参数)

条件变量

不支持

支持(通过 Condition 实现多条件等待)

性能

低竞争时接近 ReentrantLock,高竞争时略差

高竞争时性能更稳定

最佳实践

  • 简单同步场景(如单例模式、简单计数器):优先使用synchronized(语法简洁,不易出错);
  • 复杂场景(如需要中断、超时等待、多条件唤醒):使用ReentrantLock;
  • 高并发且竞争激烈的场景:根据测试结果选择(通常ReentrantLock表现更优)。

四、常见误区与性能优化

4.1 误区一:过度使用 synchronized 导致性能下降

很多开发者为 “安全起见”,盲目扩大synchronized的范围,导致锁竞争加剧。例如:

// 错误示例:同步整个方法,包含无需同步的IO操作
public synchronized void process() {// 1. 无需同步的IO操作(耗时较长)readFile();// 2. 需要同步的共享变量修改count++;
}

优化:缩小同步范围,只同步临界区:

public void process() {readFile(); // 无需同步的操作在锁外执行synchronized (lock) {count++; // 仅同步必要代码}
}

4.2 误区二:认为 synchronized 会导致死锁

synchronized本身不会导致死锁,但多把锁的无序获取会导致死锁。例如:


// 线程1:先获取lockA,再获取lockBsynchronized (lockA) {synchronized (lockB) { ... }}// 线程2:先获取lockB,再获取lockAsynchronized (lockB) {synchronized (lockA) { ... }}

避免方案

  • 所有线程按固定顺序获取锁(如先获取 lockA,再获取 lockB);
  • 使用tryLock设置超时时间,避免无限等待。

4.3 性能优化技巧

  1. 减少锁竞争
    • 拆分锁(将一个大锁拆分为多个小锁,如ConcurrentHashMap的分段锁);
    • 使用无锁数据结构(如AtomicInteger替代synchronized计数器)。
  1. 合理利用偏向锁
    • 单线程场景下,确保偏向锁未被禁用(-XX:+UseBiasedLocking,JDK 6 + 默认开启);
    • 避免频繁创建线程导致偏向锁频繁撤销(可通过-XX:BiasedLockingStartupDelay=0取消偏向锁延迟)。
  1. 控制轻量级锁自旋次数
    • 高 CPU 场景下,可适当增加自旋次数(-XX:PreBlockSpin=20);
    • 低 CPU 场景下,减少自旋次数,避免浪费 CPU。

五、总结:synchronized 的进化与未来

从 JDK 1.0 的重量级锁到 JDK 6 的锁升级机制,synchronized的进化史就是 Java 并发性能优化的缩影。它的核心价值在于简单可靠—— 即使是新手也能通过它写出线程安全的代码,而锁升级机制又为其在高并发场景下的性能提供了保障。

理解synchronized的关键不仅在于其语法使用,更在于掌握锁升级的底层逻辑:

  • 偏向锁是 “无竞争时的偷懒策略”,最大化减少无竞争开销;
  • 轻量级锁是 “轻度竞争时的折中方案”,用自旋换取内核态切换成本;
  • 重量级锁是 “重度竞争时的无奈之举”,通过操作系统机制保证线程安全。

在实际开发中,没有 “最优” 的锁,只有 “最合适” 的锁。根据业务场景的竞争强度选择同步机制,才能在安全性和性能之间找到最佳平衡。下一篇文章,我们将深入探讨Lock接口及其实现类,对比其与synchronized的设计差异,揭示 Java 并发工具的更多可能性。

http://www.dtcms.com/a/309324.html

相关文章:

  • VSCode:通义灵码插件安装使用 -- 免费AI编程工具
  • 登录校验一
  • 抢占先机,PostgreSQL 中级专家认证的职业跃迁
  • 逻辑回归在银行贷款审批中的应用:参数选择与实践
  • grafana/lock-stack 日志 Pipeline 配置
  • 性能监控体系:InfluxDB Grafana Prometheus
  • 【东枫科技】DreamHAT+
  • 3D 建模核心术语扫盲:拓扑、UV 展开、烘焙与 AO 贴图解析
  • 关于“PromptPilot” 之5 -标签词与标签动作的语言模型九宫格
  • c#中switch case语句的用法
  • Go语言的gRPC教程-拦截器
  • 向华为学习——IPD流程体系之IPD术语
  • 译 | BBC Studios团队:贝叶斯合成控制方法SCM的应用案例
  • k8s云原生rook-ceph pvc快照与恢复(上)
  • JavaScriptAJAX异步请求:XHR、Fetch与Axios对比
  • 学习笔记:封装和单继承
  • ls hgfs提示ls: cannot access ‘hgfs‘: Permission denied
  • Spring Boot 2.1.18 集成 Elasticsearch 6.6.2 实战指南
  • OneCode3.0 DSM:领域驱动设计驱动下的自定义枚举领域划分实践
  • CMake Debug/Release配置生成器表达式解析
  • 加密与安全
  • ACM SIGCOMM 2024论文精选-01:5G【Prism5G】
  • 让 OAuth 授权码流程更安全的 PKCE 技术详解
  • Unity相机控制
  • C#线程同步(三)线程安全
  • LT3045EDD#TRPBF ADI亚德诺半导体 线性稳压器 电源管理应用设计
  • PCB 控深槽如何破解 5G 基站 120℃高热魔咒?
  • Webhook是什么
  • 【Nginx反向代理】通过Nginx反向代理将多个后端server统一到同一个端口上的方法
  • 开源爬虫管理工具