17_synchronized关键字深度解析
发布时间:2026/6/5 13:56:07
分类:文化教育
浏览:1234

synchronized关键字深度解析 —— 从对象锁到锁升级文章目录synchronized关键字深度解析 —— 从对象锁到锁升级前言一、线程安全问题回顾1.1 并发问题的经典场景1.2 synchronized的解决方案二、synchronized的三种使用方式2.1 修饰实例方法2.2 修饰静态方法2.3 修饰代码块锁定对象与本身方法锁互斥的陷阱三、synchronized的底层原理3.1 对象头中的Mark Word3.2 Monitor机制重量级锁3.3 可重入性验证四、锁升级过程锁膨胀4.1 偏向锁Biased Locking4.2 轻量级锁Lightweight Locking4.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{privatestaticintcounter0;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(()-{for(inti0;i10000;i){counter;// 非原子操作}});Threadt2newThread(()-{for(inti0;i10000;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{privatestaticintcounter0;privatestaticfinalObjectlocknewObject();publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(()-{for(inti0;i10000;i){synchronized(lock){counter;}}});Threadt2newThread(()-{for(inti0;i10000;i){synchronized(lock){counter;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(结果: counter);// 始终是 20000}}二、synchronized的三种使用方式synchronized可以修饰三种目标实例方法、静态方法、代码块。理解锁的是什么是使用synchronized的第一课——很多人写出了加锁代码但线程安全问题依然存在原因就是锁错了对象。核心记忆法则修饰实例方法 → 锁是this当前实例对象不同实例各锁各的互不影响修饰静态方法 → 锁是类名.classClass对象所有实例共享同一把锁修饰代码块 → 锁是括号中指定的任意对象灵活但容易出错2.1 修饰实例方法锁住的是当前实例对象this。不同实例的锁互不影响。publicclassSyncOnInstanceMethod{privateintcount0;// 等价于 synchronized(this) 包裹整个方法体publicsynchronizedvoidincrement(){count;}publicsynchronizedintgetCount(){returncount;}publicstaticvoidmain(String[]args)throwsInterruptedException{SyncOnInstanceMethoddemonewSyncOnInstanceMethod();// 多个线程操作同一个对象Threadt1newThread(()-{for(inti0;i10000;i)demo.increment();});Threadt2newThread(()-{for(inti0;i10000;i)demo.increment();});t1.start();t2.start();t1.join();t2.join();System.out.println(demo.getCount());// 20000}}2.2 修饰静态方法锁住的是类的Class对象。同一个类的所有实例共享同一把锁。publicclassSyncOnStaticMethod{privatestaticintcount0;// 等价于 synchronized(SyncOnStaticMethod.class)publicstaticsynchronizedvoidincrement(){count;}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(()-{for(inti0;i10000;i)increment();});Threadt2newThread(()-{for(inti0;i10000;i)increment();});t1.start();t2.start();t1.join();t2.join();System.out.println(count);// 20000}}2.3 修饰代码块锁住的是指定的对象。这是最灵活的方式可以精确控制锁的粒度。publicclassSyncBlockDemo{privatefinalObjectreadLocknewObject();privatefinalObjectwriteLocknewObject();privateStringBuilderdatanewStringBuilder();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){SyncBlockDemodemonewSyncBlockDemo();// 读和写使用不同的锁互不影响提高并发度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] 其中: lock01 表示无锁 偏向锁: [thread:54][epoch:2][unused:1][age:4][biased_lock:1][lock:2] 其中: biased_lock1, lock01 表示偏向锁 轻量级锁: [ptr_to_lock_record:62][lock:2] 其中: lock00 表示轻量级锁 重量级锁: [ptr_to_monitor:62][lock:2] 其中: lock10 表示重量级锁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的STWStop The World阶段可能会被降级。4.1 偏向锁Biased Locking偏向锁认为大多数情况下锁不仅不存在竞争而且总是由同一个线程多次获取。当线程第一次获取锁时Mark Word中记录偏向线程ID。此后该线程再次进入同步块时只需检查Mark Word中的线程ID是否是自己如果是则直接进入——不需要CAS操作也不需要monitor。注意偏向锁并非免费的——如果锁确实存在多线程竞争撤销偏向锁本身也需要开销需要到达安全点即STW。这也解释了为什么JDK 15默认关闭了偏向锁在现代高并发应用中锁竞争比过去更常见偏向锁带来的收益不如其撤销开销。在实际项目中如果你的应用确实存在大量单线程使用的锁可以通过JVM参数手动开启。publicclassBiasedLockDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ObjectlocknewObject();// 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:BiasedLockingStartupDelay0关闭偏向锁启动延迟4.2 轻量级锁Lightweight Locking当有第二个线程尝试获取偏向锁时偏向锁会撤销升级为轻量级锁。轻量级锁采用**CASCompare And Swap**自旋尝试获取锁publicclassLightweightLockDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ObjectlocknewObject();Threadt1newThread(()-{synchronized(lock){System.out.println(t1 获取锁偏向锁);try{Thread.sleep(2000);}catch(InterruptedExceptione){}}});Threadt2newThread(()-{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(){StringBuffersbnewStringBuffer();sb.append(Hello );sb.append(World);returnsb.toString();}// 等效于使用 StringBuilder线程不安全—— 无锁}4.5 锁粗化JVM会将连续的加锁解锁合并为范围更大的锁减少加解锁次数publicclassLockCoarsening{privatefinalObjectlocknewObject();// 优化前反复加锁解锁publicvoidbeforeOptimize(){for(inti0;i100;i){synchronized(lock){// 小操作}}}// JVM自动粗化为publicvoidafterOptimize(){synchronized(lock){for(inti0;i100;i){// 小操作}}}}五、synchronized vs Lock对比维度synchronizedLock实现层面JVM关键字C nativeJDK纯Java实现锁获取隐式获取释放显式手动 lock()/unlock()可中断不支持支持 lockInterruptibly()超时获取不支持支持 tryLock(timeout)公平性非公平可选择公平/非公平多条件单个 (wait/notify)多个Condition性能JDK 6后差距很小同易用性简单需手动释放六、经典案例多线程卖票publicclassTicketSelling{privateinttickets100;publicsynchronizedbooleansellTicket(){if(tickets0){// 模拟出票耗时try{Thread.sleep(10);}catch(InterruptedExceptione){}System.out.println(Thread.currentThread().getName() 售出第 (tickets--) 张票);returntrue;}returnfalse;}publicstaticvoidmain(String[]args){TicketSellingstationnewTicketSelling();for(inti1;i4;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/MutexJVM的五种锁优化策略自适应自旋、锁消除逃逸分析、锁粗化、偏向锁延迟、轻量级锁CASsynchronized与Lock的全面对比可中断性、超时获取、公平性、多条件队列等方面的差异适用场景多线程售票系统、库存扣减等需要原子性保护的经典并发场景单例模式的线程安全实现懒汉式synchronized方法或DCL volatile对代码侵入性要求低、追求简洁可靠性的业务同步场景扩展方向深入学习ReentrantLock的高级特性可中断lockInterruptibly()、超时tryLock()、公平锁研究JUC原子类AtomicInteger、LongAdder的无锁CAS并发方案推荐阅读18_Java中的Lock锁机制下一篇18_Java中的Lock锁机制