☰
17_synchronized关键字深度解析
2026/10/11 9:26:04 网站建设 项目流程

synchronized关键字深度解析 —— 从对象锁到锁升级

文章目录

  • synchronized关键字深度解析 —— 从对象锁到锁升级
    • 前言
    • 一、线程安全问题回顾
      • 1.1 并发问题的经典场景
      • 1.2 synchronized的解决方案
    • 二、synchronized的三种使用方式
      • 2.1 修饰实例方法
      • 2.2 修饰静态方法
      • 2.3 修饰代码块
      • 锁定对象与本身方法锁互斥的陷阱
    • 三、synchronized的底层原理
      • 3.1 对象头中的Mark Word
      • 3.2 Monitor机制(重量级锁)
      • 3.3 可重入性验证
    • 四、锁升级过程(锁膨胀)
      • 4.1 偏向锁(Biased Locking)
      • 4.2 轻量级锁(Lightweight Locking)
      • 4.3 自适应自旋锁
      • 4.4 锁消除
      • 4.5 锁粗化
    • 五、synchronized vs Lock
    • 六、经典案例:多线程卖票
    • 总结
    • ✅ 亮点总结
    • 适用场景
    • 扩展方向

前言

在Java并发编程中,synchronized关键字是解决线程安全问题最基础、最常用的手段。几乎所有Java面试都会问到它的底层原理、锁升级过程、以及与Lock的区别。

很多人对synchronized的认知停留在"给代码块加锁"的层面,但它的底层机制远比表面复杂。synchronized从JDK 1.0就存在了,但在JDK 1.6之前,它确实被称为"重量级锁"——每次加锁都需要操作系统级别的monitor操作,性能很差。JDK 1.6进行了"synchronized性能革命",引入了偏向锁、轻量级锁、自适应自旋、锁消除、锁粗化等一系列优化,使得synchronized在大多数场景下的性能已经不输于ReentrantLock。这也是为什么JDK 1.8的ConcurrentHashMap放弃了JDK 1.7的分段锁(Segment + ReentrantLock),转而使用synchronized + CAS的组合。

本文将带你深入理解synchronized的方方面面:三种使用方式、底层Monitor机制、Mark Word在对象头中的变化、以及从偏向锁到轻量级锁再到重量级锁的完整升级过程。

一、线程安全问题回顾

1.1 并发问题的经典场景

publicclassThreadSafetyIssue{privatestaticintcounter=0;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(()->{for(inti=0;i<10000;i++){counter++;// 非原子操作}});Threadt2=newThread(()->{for(inti=0;i<10000;i++){counter++;}});t1.start();t2.start();t1.join();t2.join();System.out.println("期望值: 20000, 实际值: "+counter);// 每次运行结果可能不同,典型输出:17650、18432等}}

counter++看似一行代码,实际是三个步骤:读取 → 加1 → 写回。多线程交替执行这三个步骤,导致结果不确定。

这种问题被称为竞态条件(Race Condition)——多个线程同时访问共享数据,且至少有一个线程在修改数据,最终结果取决于线程执行的精确时序。这种bug的特点是非常难以复现和调试:你可能本地测试100次都正常,上了生产环境偶尔出现一次数据错误。这也是为什么理解线程安全原理比"能用synchronized"重要得多——你得知道什么场景下需要加锁、加在什么地方、加多大范围。

1.2 synchronized的解决方案

publicclassSynchronizedSolution{privatestaticintcounter=0;privatestaticfinalObjectlock=newObject();publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(()->{for(inti=0;i<10000;i++){synchronized(lock){counter++;}}});Threadt2=newThread(()->{for(inti=0;i<10000;i++){synchronized(lock){counter++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("结果: "+counter);// 始终是 20000}}

二、synchronized的三种使用方式

synchronized可以修饰三种目标:实例方法、静态方法、代码块。理解"锁的是什么"是使用synchronized的第一课——很多人写出了加锁代码但线程安全问题依然存在,原因就是锁错了对象。

核心记忆法则:

  • 修饰实例方法 → 锁是this(当前实例对象),不同实例各锁各的,互不影响
  • 修饰静态方法 → 锁是类名.class(Class对象),所有实例共享同一把锁
  • 修饰代码块 → 锁是括号中指定的任意对象,灵活但容易出错

2.1 修饰实例方法

锁住的是当前实例对象(this)。不同实例的锁互不影响。

publicclassSyncOnInstanceMethod{privateintcount=0;// 等价于 synchronized(this) 包裹整个方法体publicsynchronizedvoidincrement(){count++;}publicsynchronizedintgetCount(){returncount;}publicstaticvoidmain(String[]args)throwsInterruptedException{SyncOnInstanceMethoddemo=newSyncOnInstanceMethod();// 多个线程操作同一个对象Threadt1=newThread(()->{for(inti=0;i<10000;i++)demo.increment();});Threadt2=newThread(()->{for(inti=0;i<10000;i++)demo.increment();});t1.start();t2.start();t1.join();t2.join();System.out.println(demo.getCount());// 20000}}

2.2 修饰静态方法

锁住的是类的Class对象。同一个类的所有实例共享同一把锁。

publicclassSyncOnStaticMethod{privatestaticintcount=0;// 等价于 synchronized(SyncOnStaticMethod.class)publicstaticsynchronizedvoidincrement(){count++;}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(()->{for(inti=0;i<10000;i++)increment();});Threadt2=newThread(()->{for(inti=0;i<10000;i++)increment();});t1.start();t2.start();t1.join();t2.join();System.out.println(count);// 20000}}

2.3 修饰代码块

锁住的是指定的对象。这是最灵活的方式,可以精确控制锁的粒度。

publicclassSyncBlockDemo{privatefinalObjectreadLock=newObject();privatefinalObjectwriteLock=newObject();privateStringBuilderdata=newStringBuilder();publicvoidread(){synchronized(readLock){System.out.println(Thread.currentThread().getName()+" 读取数据: "+data);}}publicvoidwrite(Stringcontent){synchronized(writeLock){data.append(content);System.out.println(Thread.currentThread().getName()+" 写入数据: "+content);}}publicstaticvoidmain(String[]args){SyncBlockDemodemo=newSyncBlockDemo();// 读和写使用不同的锁,互不影响,提高并发度newThread(()->demo.read(),"读线程").start();newThread(()->demo.write("Hello"),"写线程").start();newThread(()->demo.read(),"读线程2").start();}}

锁定对象与本身方法锁互斥的陷阱

// 实例方法锁和锁this是同一把锁,会互斥// 静态方法锁和锁XXXX.class是同一把锁,会互斥// 实例锁和类锁是两把不同的锁,不会互斥publicclassLockInteraction{publicsynchronizedvoidmethod1(){System.out.println("实例方法锁");}publicvoidmethod2(){synchronized(this){System.out.println("锁this");}}// method1和method2是同一把锁,会互斥publicstaticsynchronizedvoidstaticMethod(){System.out.println("静态方法锁");}// staticMethod和上面method1/method2是不同的锁,不互斥}

三、synchronized的底层原理

3.1 对象头中的Mark Word

每个Java对象在JVM中都有一个对象头,其中Mark Word记录了锁状态、GC分代年龄、HashCode等信息。

Mark Word 在不同锁状态下的结构(64位JVM): 无锁状态: [unused:25][hashcode:31][unused:1][age:4][biased_lock:1][lock:2] 其中: lock=01 表示无锁 偏向锁: [thread:54][epoch:2][unused:1][age:4][biased_lock:1][lock:2] 其中: biased_lock=1, lock=01 表示偏向锁 轻量级锁: [ptr_to_lock_record:62][lock:2] 其中: lock=00 表示轻量级锁 重量级锁: [ptr_to_monitor:62][lock:2] 其中: lock=10 表示重量级锁

3.2 Monitor机制(重量级锁)

重量级锁基于操作系统的Monitor(管程)机制实现,依赖于操作系统的Mutex Lock(互斥锁):

// synchronized底层会生成 monitorenter 和 monitorexit 指令// 对应 C++ 层面的 ObjectMonitor 对象// 伪代码表示:// monitorenter: 尝试获取对象的monitor// 如果monitor的计数器为0,获取成功,计数器+1// 如果当前线程已经持有monitor,计数器再+1(可重入)// 否则,线程阻塞,等待monitor被释放// monitorexit: 释放monitor// 计数器-1// 如果减到0,唤醒等待的线程

3.3 可重入性验证

synchronized是可重入锁——同一线程可以多次获取同一把锁:

publicclassReentrantDemo{publicsynchronizedvoidmethodA(){System.out.println("进入方法A");methodB();// 在持有锁的情况下再次加锁——可重入System.out.println("离开方法A");}publicsynchronizedvoidmethodB(){System.out.println("进入方法B");// 嵌套加锁同样支持synchronized(this){System.out.println("进入方法B的同步块");}}publicstaticvoidmain(String[]args){newReentrantDemo().methodA();}}/* 输出: 进入方法A 进入方法B 进入方法B的同步块 离开方法A */

四、锁升级过程(锁膨胀)

JDK 1.6之后,synchronized做了重大优化,锁的状态会随着竞争情况自动升级。这是synchronized面试题的重中之重。整个升级过程体现了JVM对"大部分锁竞争不会发生"这一假设的利用——先以最低成本的锁(偏向锁)处理最乐观的情况,随着竞争加剧逐步升级为成本更高的锁。

锁升级的核心思想:先用最轻的锁,不行再加码。就像你去图书馆占座——如果只有你一个人去,贴张纸条(偏向锁)就够了;偶尔有人跟你抢,你站旁边等一下(轻量级锁自旋);天天有人跟你抢,就得去前台登记排队了(重量级锁)。

无锁 ──→ 偏向锁 ──→ 轻量级锁 ──→ 重量级锁 (逐渐升级,不可降级)

注意:锁升级是单向的、不可逆的。一旦升级到轻量级锁或重量级锁,即使后续竞争消失,也不会降回偏向锁。但有一个例外——重量级锁在GC的STW(Stop The World)阶段可能会被降级。

4.1 偏向锁(Biased Locking)

偏向锁认为大多数情况下锁不仅不存在竞争,而且总是由同一个线程多次获取。当线程第一次获取锁时,Mark Word中记录偏向线程ID。此后该线程再次进入同步块时,只需检查Mark Word中的线程ID是否是自己,如果是则直接进入——不需要CAS操作,也不需要monitor。

注意:偏向锁并非"免费"的——如果锁确实存在多线程竞争,撤销偏向锁本身也需要开销(需要到达安全点,即STW)。这也解释了为什么JDK 15+默认关闭了偏向锁:在现代高并发应用中,锁竞争比过去更常见,偏向锁带来的收益不如其撤销开销。在实际项目中,如果你的应用确实存在大量单线程使用的锁,可以通过JVM参数手动开启。

publicclassBiasedLockDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Objectlock=newObject();// JVM默认偏向锁会延迟4秒开启Thread.sleep(5000);// 第一次获取锁,升级为偏向锁synchronized(lock){System.out.println("线程1持有偏向锁");System.out.println("Mark Word: "+ClassLayout.parseInstance(lock).toPrintable());}}}

JVM参数控制:

  • -XX:+UseBiasedLocking:启用偏向锁(JDK 15后默认关闭)
  • -XX:BiasedLockingStartupDelay=0:关闭偏向锁启动延迟

4.2 轻量级锁(Lightweight Locking)

当有第二个线程尝试获取偏向锁时,偏向锁会撤销升级为轻量级锁。轻量级锁采用**CAS(Compare And Swap)**自旋尝试获取锁:

publicclassLightweightLockDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Objectlock=newObject();Threadt1=newThread(()->{synchronized(lock){System.out.println("t1 获取锁(偏向锁)");try{Thread.sleep(2000);}catch(InterruptedExceptione){}}});Threadt2=newThread(()->{synchronized(lock){System.out.println("t2 获取锁(升级为轻量级锁)");// t2在等待期间会自旋尝试,不会立即阻塞}});t1.start();Thread.sleep(100);// 确保t1先获取锁t2.start();t1.join();t2.join();}}

轻量级锁的特点:

  • 适用于线程交替执行同步块的场景
  • 自旋会消耗CPU,但避免了线程切换的开销
  • 如果自旋超过一定次数(默认10次,JDK 6改为自适应),升级为重量级锁

4.3 自适应自旋锁

JDK 6引入了自适应自旋,自旋时间不再固定,而是根据上次在同一锁上的自旋时间及锁持有者的状态动态调整:

// 自适应自旋策略:// - 如果之前自旋成功获取过锁,JVM会让自旋时间更长// - 如果之前自旋很少成功,JVM会减少自旋甚至直接阻塞// 让JVM自己根据实际情况来决定,开发者无需手动调优

4.4 锁消除

JIT编译时,JVM通过逃逸分析判断某些锁不可能发生竞争,直接将其消除:

publicclassLockElimination{// StringBuffer的append()方法加了synchronized// 但sb对象不会逃逸出方法,JVM会消除锁publicStringconcat(){StringBuffersb=newStringBuffer();sb.append("Hello ");sb.append("World");returnsb.toString();}// 等效于使用 StringBuilder(线程不安全)—— 无锁}

4.5 锁粗化

JVM会将连续的加锁解锁合并为范围更大的锁,减少加解锁次数:

publicclassLockCoarsening{privatefinalObjectlock=newObject();// 优化前:反复加锁解锁publicvoidbeforeOptimize(){for(inti=0;i<100;i++){synchronized(lock){// 小操作}}}// JVM自动粗化为:publicvoidafterOptimize(){synchronized(lock){for(inti=0;i<100;i++){// 小操作}}}}

五、synchronized vs Lock

对比维度synchronizedLock
实现层面JVM关键字,C++ nativeJDK纯Java实现
锁获取隐式获取释放显式手动 lock()/unlock()
可中断不支持支持 lockInterruptibly()
超时获取不支持支持 tryLock(timeout)
公平性非公平可选择公平/非公平
多条件单个 (wait/notify)多个Condition
性能JDK 6后差距很小同
易用性简单需手动释放

六、经典案例:多线程卖票

publicclassTicketSelling{privateinttickets=100;publicsynchronizedbooleansellTicket(){if(tickets>0){// 模拟出票耗时try{Thread.sleep(10);}catch(InterruptedExceptione){}System.out.println(Thread.currentThread().getName()+" 售出第 "+(tickets--)+" 张票");returntrue;}returnfalse;}publicstaticvoidmain(String[]args){TicketSellingstation=newTicketSelling();for(inti=1;i<=4;i++){finalStringwindowName="窗口"+i;newThread(()->{while(station.sellTicket()){// 持续卖票直到售罄}},windowName).start();}}}

总结

synchronized从JDK 1.6开始已经不再是那个"重量级"的代名词。偏向锁、轻量级锁、自适应自旋、锁粗化、锁消除等一系列优化,让synchronized的性能在许多场景下已经不输于ReentrantLock。

理解锁升级的完整过程(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)是面试中的核心考点。记住这个口诀:单线程用偏向锁,交替执行用轻量级锁,激烈竞争才用重量级锁。JVM通过这些优化策略,在保证线程安全的同时,尽可能地减少了锁带来的性能开销。

核心知识回顾:

  • 三种使用方式:实例方法锁(锁this)、静态方法锁(锁Class对象)、代码块锁(锁指定对象)。实例锁和类锁互不影响——这是容易混淆的设计点。
  • Monitor机制:每个对象都有一个关联的Monitor对象,synchronized的加锁解锁底层就是monitorenter/monitorexit指令。Monitor内部有计数器实现可重入性。
  • Mark Word:对象头中的核心字段,不同锁状态对应不同的bit位结构。了解Mark Word是理解锁升级的前提。
  • 锁优化:自适应自旋(根据历史决定自旋时长)、锁消除(逃逸分析)、锁粗化(合并连续加锁)、偏向锁延迟(默认4秒后开启)

最后提醒:在JDK 1.8及之后的版本中,对于大多数场景,首选synchronized——它语法简洁、自动释放(不会忘记unlock)、JVM持续优化。只有在需要可中断的锁获取(lockInterruptibly)、超时尝试(tryLock)或公平锁等特殊需求时,才使用ReentrantLock。

✅ 亮点总结

  • synchronized的三种使用方式(实例方法/静态方法/代码块)及各自锁对象的精确辨析,实例锁与类锁互不影响
  • Mark Word在无锁、偏向锁、轻量级锁、重量级锁四种状态下的64位结构变化图解
  • 锁升级全链路机制:单线程偏向锁→交替执行轻量级锁(CAS自旋)→激烈竞争重量级锁(monitor/Mutex)
  • JVM的五种锁优化策略:自适应自旋、锁消除(逃逸分析)、锁粗化、偏向锁延迟、轻量级锁CAS
  • synchronized与Lock的全面对比:可中断性、超时获取、公平性、多条件队列等方面的差异

适用场景

  • 多线程售票系统、库存扣减等需要原子性保护的经典并发场景
  • 单例模式的线程安全实现(懒汉式synchronized方法或DCL + volatile)
  • 对代码侵入性要求低、追求简洁可靠性的业务同步场景

扩展方向

  • 深入学习ReentrantLock的高级特性(可中断lockInterruptibly()、超时tryLock()、公平锁)
  • 研究JUC原子类(AtomicInteger、LongAdder)的无锁CAS并发方案
  • 推荐阅读:18_Java中的Lock锁机制

下一篇:18_Java中的Lock锁机制

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询