当前位置: 首页 > 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.lryc.cn/news/607671.html

相关文章:

  • 什么是Sedex审核?Sedex审核的主要内容,Sedex审核的流程
  • 通用障碍物调研
  • 【C++进阶】一文吃透静态绑定、动态绑定与多态底层机制(含虚函数、vptr、thunk、RTTI)
  • 测试分类:详解各类测试方式与方法
  • 使用gcc代替v语言的tcc编译器提高编译后二进制文件执行速度
  • Trust Management System (TMS)
  • MySQL锁的分类 MVCC和S/X锁的互补关系
  • Linux编程: 10、线程池与初识网络编程
  • GESP2025年6月认证C++八级( 第三部分编程题(1)树上旅行)
  • 链表【各种题型+对应LeetCode习题练习】
  • 《C++》STL--list容器详解
  • UnionApplication
  • 江协科技STM32 12-2 BKP备份寄存器RTC实时时钟
  • 【Shell脚本自动化编写——报警邮件,检查磁盘,web服务检测】
  • Windows安装虚拟机遇到内容解码失败
  • python-异常(笔记)
  • Java学习-运算符
  • Java:JWT 从原理到高频面试题解析
  • 【Linux】重生之从零开始学习运维之Mysql
  • Rust在CentOS 6上的移植
  • 2025.8.1
  • 1661. 每台机器的进程平均运行时间
  • 系统开机时自动执行指令
  • 基于python大数据的招聘数据可视化及推荐系统
  • 算法思想之 多源 BFS 问题
  • 【Node.js安装注意事项】-安装路径不能有空格
  • PNP机器人机器人学术年会展示灵巧手动作捕捉方案。
  • MySQL分析步
  • Android签名轮转
  • Conda install安装了一些库,如何撤销操作