文章目录Java并发编程synchronized锁机制 系统性知识体系一、synchronized基础概述1.1 核心作用1.2 三种使用方式二、synchronized底层实现原理2.1 字节码层面实现2.2 对象监视器Monitor原理三、Java对象头详解锁实现的基础3.1 对象头结构32位3.2 Mark Word的状态变化四、完整锁升级流程JDK 1.64.1 偏向锁Biased Locking4.2 轻量级锁Lightweight Locking4.3 重量级锁Heavyweight Locking4.4 锁升级完整流程图五、JVM对synchronized的锁优化技术5.1 自旋锁与自适应自旋5.2 锁消除5.3 锁粗化六、synchronized的可重入性6.1 可重入性定义6.2 可重入性实现原理6.3 可重入性的意义七、synchronized与ReentrantLock的对比八、核心考点与常见误区8.1 核心考点8.2 常见误区Java synchronized锁机制 面试版核心考点清单可直接背诵一、基础必背考点二、对象头核心考点32位HotSpot三、锁升级全流程绝对高频考点四、JVM锁优化技术五、常见误区澄清3道高频面试题及标准答案面试题1详细描述synchronized的锁升级全过程面试题2synchronized和ReentrantLock有什么区别面试题3什么是偏向锁它的撤销过程是怎样的为什么需要STWJava并发编程synchronized锁机制 系统性知识体系一、synchronized基础概述1.1 核心作用原子性保证临界区代码的原子执行防止多线程交错执行导致的数据不一致可见性根据JMM规范锁释放时会将工作内存数据刷新到主内存锁获取时会清空工作内存并从主内存重新加载有序性禁止临界区内部代码与外部代码的指令重排序临界区内部仍允许重排序1.2 三种使用方式使用方式锁对象作用范围修饰实例方法this当前实例对象同一实例的所有同步方法互斥修饰静态方法类的Class对象该类所有实例的静态同步方法互斥修饰代码块括号内指定的对象同一锁对象的所有同步代码块互斥二、synchronized底层实现原理2.1 字节码层面实现synchronized在字节码层面通过两个指令实现monitorenter进入同步块尝试获取对象监视器monitor的所有权monitorexit退出同步块释放对象监视器的所有权关键细节编译器会自动在同步代码块前后插入这两个指令为了保证异常时也能释放锁编译器会生成一个异常表在异常发生时自动执行monitorexit每个对象都关联一个monitor当monitor被占用时就处于锁定状态2.2 对象监视器Monitor原理Monitor是操作系统提供的同步原语在HotSpot中由ObjectMonitor实现核心结构ObjectMonitor{_owner// 持有当前锁的线程指针_count// 锁计数器支持可重入性_waiters// 等待队列调用wait()的线程_entryList// 入口队列竞争锁失败的线程_recursions// 重入次数}执行流程线程执行monitorenter时检查_count是否为0若为0将_owner设为当前线程_count加1若不为0且_owner是当前线程_count加1可重入若不为0且_owner不是当前线程线程进入_entryList阻塞执行monitorexit时_count减1减到0时释放锁并唤醒_entryList中的线程三、Java对象头详解锁实现的基础在32位HotSpot虚拟机中对象在内存中分为三部分对象头、实例数据、对齐填充。其中对象头是synchronized锁机制的核心载体。3.1 对象头结构32位长度内容说明32bitMark Word存储对象的哈希码、GC分代年龄、锁状态标志等32bitKlass Pointer指向对象所属类的元数据的指针可选数组长度只有数组对象才有占32bit3.2 Mark Word的状态变化Mark Word是一个动态的数据结构会根据对象的锁状态复用存储空间不同状态下的内容如下锁状态25bit4bit1bit2bit无锁对象哈希码GC分代年龄001偏向锁线程IDGC分代年龄101轻量级锁指向栈中锁记录的指针--00重量级锁指向monitor对象的指针--10GC标记空--1164位Mark Word说明线程ID占54bitGC分代年龄占4bit偏向锁标志1bit锁标志2bit开启指针压缩时Klass Pointer占32bit否则占64bit四、完整锁升级流程JDK 1.6JDK 1.6对synchronized进行了重大优化引入了偏向锁和轻量级锁使得锁不再直接升级为重量级锁而是按照偏向锁→轻量级锁→重量级锁的顺序逐步升级且升级后不可降级。4.1 偏向锁Biased Locking设计思想大多数情况下锁不仅不存在多线程竞争而且总是由同一个线程多次获得。因此可以让锁偏向于第一个获取它的线程后续该线程获取锁时无需任何同步操作。获取流程检查对象头Mark Word中的锁标志位是否为01偏向锁标志是否为1如果是可偏向状态检查Mark Word中的线程ID是否为当前线程ID如果是直接进入同步块无需任何CAS操作如果不是使用CAS操作将Mark Word中的线程ID替换为当前线程ID如果CAS成功获取偏向锁进入同步块如果CAS失败说明存在竞争触发偏向锁撤销偏向锁撤销偏向锁的撤销需要等待全局安全点STW暂停持有偏向锁的线程检查该线程是否还在执行同步块如果线程已经退出同步块将对象头恢复为无锁状态然后重新偏向如果线程仍在执行同步块升级为轻量级锁注意偏向锁默认是开启的但在应用启动后几秒钟才会激活。可以通过-XX:-UseBiasedLocking关闭偏向锁。4.2 轻量级锁Lightweight Locking设计思想当存在多个线程交替执行同步块时使用用户态的CAS操作代替内核态的互斥量避免线程切换的开销。获取流程线程在自己的栈帧中创建一个名为**锁记录Lock Record**的空间将对象头的Mark Word复制到锁记录中称为Displaced Mark Word使用CAS操作尝试将对象头的Mark Word替换为指向当前线程锁记录的指针如果CAS成功获取轻量级锁进入同步块如果CAS失败检查对象头的Mark Word是否指向当前线程的锁记录如果是说明是重入直接进入同步块如果不是说明存在竞争自旋等待一定次数如果自旋后仍未获取到锁升级为重量级锁释放流程使用CAS操作将锁记录中的Displaced Mark Word替换回对象头如果CAS成功释放锁完成如果CAS失败说明锁已经升级为重量级锁进入重量级锁的释放流程4.3 重量级锁Heavyweight Locking设计思想当存在大量线程同时竞争锁时轻量级锁的自旋会消耗大量CPU资源此时升级为依赖操作系统互斥量的重量级锁。执行流程锁升级为重量级锁后对象头的Mark Word指向操作系统的monitor对象竞争失败的线程会被操作系统挂起进入阻塞状态当锁被释放时操作系统会唤醒所有等待的线程重新竞争锁竞争失败的线程再次被挂起如此循环性能特点优点不会消耗CPU资源进行自旋缺点线程阻塞和唤醒需要操作系统内核态和用户态的切换开销很大4.4 锁升级完整流程图无锁状态 ↓ 偏向锁单线程访问 ↓ 发生竞争 轻量级锁多线程交替访问 ↓ 竞争加剧/自旋失败 重量级锁多线程同时竞争五、JVM对synchronized的锁优化技术5.1 自旋锁与自适应自旋自旋锁线程竞争锁失败时不立即阻塞而是循环执行几次空操作等待锁释放避免了线程切换的开销但会消耗CPU资源适用于锁持有时间很短的场景自适应自旋JDK 1.6引入自旋次数不再固定而是由前一次在同一个锁上的自旋时间和锁持有者的状态决定如果在同一个锁对象上自旋等待刚刚成功获得过锁并且持有锁的线程正在运行中那么虚拟机会认为这次自旋也很可能成功从而允许自旋更长的时间如果对于某个锁自旋很少成功过那么虚拟机会直接跳过自旋过程阻塞线程5.2 锁消除定义JIT编译器在运行时对一些代码上要求同步但实际上不可能存在共享数据竞争的锁进行消除。判断依据基于逃逸分析技术如果一个对象不会逃逸出当前线程那么这个对象的锁操作可以被安全地消除。示例publicStringconcatString(Strings1,Strings2,Strings3){// StringBuffer是线程安全的append方法是synchronized的StringBuffersbnewStringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);returnsb.toString();}上述代码中sb对象是局部变量不会逃逸出方法因此JIT编译器会消除所有append方法的锁操作5.3 锁粗化定义将多个连续的加锁和解锁操作合并成一个更大范围的加锁和解锁操作减少频繁的锁操作带来的开销。示例// 优化前for(inti0;i100;i){synchronized(lock){// 操作}}// 优化后synchronized(lock){for(inti0;i100;i){// 操作}}注意锁粗化也有边界如果循环内有耗时操作粗化会导致锁持有时间过长反而降低并发性。六、synchronized的可重入性6.1 可重入性定义可重入性是指同一个线程已经获取了某个锁之后再次请求获取该锁时不会被阻塞。6.2 可重入性实现原理每个锁对象都有一个锁计数器和一个持有线程指针当线程第一次获取锁时计数器加1持有线程指针指向该线程当同一个线程再次获取该锁时计数器再次加1当线程释放锁时计数器减1当计数器减到0时锁被完全释放其他线程可以获取6.3 可重入性的意义避免了死锁如果没有可重入性子类重写父类的同步方法并调用父类方法时会发生死锁简化了编程模型程序员不需要担心自己已经获取的锁会导致自己阻塞示例publicclassReentrantExample{publicsynchronizedvoidmethodA(){System.out.println(执行methodA);methodB();// 同一个线程再次获取锁不会阻塞}publicsynchronizedvoidmethodB(){System.out.println(执行methodB);}}七、synchronized与ReentrantLock的对比特性synchronizedReentrantLock实现方式JVM层面实现Java API层面实现可重入性支持支持公平性非公平锁支持公平锁和非公平锁可中断性不支持支持lockInterruptibly()超时获取不支持支持tryLock(long timeout)条件变量不支持wait/notify支持多个Condition对象自动释放是异常时自动释放否必须手动在finally中释放性能JDK 1.6优化后与ReentrantLock相当略高在高竞争场景下八、核心考点与常见误区8.1 核心考点对象头Mark Word在不同锁状态下的结构完整的锁升级流程及每个阶段的触发条件偏向锁、轻量级锁、重量级锁的优缺点及适用场景可重入性的实现原理JVM的各种锁优化技术8.2 常见误区❌ 错误synchronized是重量级锁性能很差✅ 正确JDK 1.6引入了偏向锁和轻量级锁在无竞争或低竞争场景下性能很高❌ 错误锁可以降级✅ 正确锁只能升级不能降级。偏向锁可以撤销但不是降级❌ 错误synchronized修饰静态方法和实例方法效果一样✅ 正确静态方法锁的是Class对象实例方法锁的是this对象两者互不影响❌ 错误自旋锁一定比阻塞锁好✅ 正确自旋锁适用于锁持有时间很短的场景如果锁持有时间很长自旋会浪费大量CPU资源Java synchronized锁机制 面试版核心考点清单可直接背诵一、基础必背考点synchronized三大特性保证原子性、可见性、有序性禁止临界区内外重排序三种使用方式及锁对象实例方法锁this当前实例静态方法锁类名.classClass对象代码块锁括号内指定的任意对象字节码实现通过monitorenter和monitorexit指令异常时自动释放锁可重入性实现每个锁对象有持有线程指针和锁计数器同一线程重入时计数器递增释放时递减减到0才真正释放二、对象头核心考点32位HotSpot对象头组成Mark Word32bit Klass Pointer32bit 数组长度仅数组对象Mark Word状态复用表必考锁状态锁标志位核心存储内容无锁01对象哈希码GC分代年龄偏向锁01偏向位1线程IDGC分代年龄轻量级锁00指向栈中锁记录的指针重量级锁10指向操作系统Monitor的指针64位差异线程ID占54bit开启指针压缩时Klass Pointer占32bit三、锁升级全流程绝对高频考点核心原则锁只能升级不能降级JDK1.6默认开启偏向锁和轻量级锁偏向锁单线程无竞争设计思想让锁偏向第一个获取它的线程后续获取无需CAS获取CAS将Mark Word中的线程ID替换为当前线程ID升级触发有其他线程竞争该锁关键偏向锁撤销需要全局安全点STW轻量级锁多线程交替执行设计思想用用户态CAS代替内核态阻塞避免线程切换开销获取线程栈创建锁记录→复制Mark Word到锁记录→CAS将对象头指向锁记录升级触发自旋失败默认10次或多个线程同时竞争关键自旋会消耗CPU适用于锁持有时间极短的场景重量级锁多线程同时竞争设计思想依赖操作系统互斥量竞争失败的线程直接阻塞实现对象头指向ObjectMonitor包含_owner、_count、_entryList、_waiters缺点线程阻塞和唤醒需要内核态/用户态切换开销大四、JVM锁优化技术自适应自旋自旋次数不固定由前一次自旋成功率和锁持有者状态决定锁消除基于逃逸分析消除不可能存在竞争的锁如局部变量的锁锁粗化将多个连续的加锁解锁操作合并为一个大锁减少频繁操作开销五、常见误区澄清❌ synchronized是重量级锁 → ✅ 无竞争时是偏向锁性能极高❌ 锁可以降级 → ✅ 只能升级偏向锁撤销≠降级❌ 静态方法和实例方法互斥 → ✅ 锁对象不同互不影响❌ 自旋一定比阻塞好 → ✅ 锁持有时间长时自旋会浪费大量CPU3道高频面试题及标准答案面试题1详细描述synchronized的锁升级全过程标准答案synchronized在JDK1.6后引入了分级锁机制锁会按照偏向锁→轻量级锁→重量级锁的顺序逐步升级初始状态对象刚创建时处于无锁可偏向状态Mark Word中偏向位为1线程ID为空。偏向锁获取当第一个线程访问同步块时通过CAS操作将Mark Word中的线程ID设置为自己的ID。如果成功该线程后续每次进入同步块都无需任何同步操作。偏向锁撤销与升级当有第二个线程竞争该锁时会触发偏向锁撤销。JVM会暂停持有偏向锁的线程检查其是否还在执行同步块如果线程已退出同步块将对象头恢复为无锁状态重新偏向新线程如果线程仍在执行同步块立即升级为轻量级锁轻量级锁获取线程在自己的栈帧中创建锁记录将对象头的Mark Word复制到锁记录中然后通过CAS尝试将对象头的Mark Word替换为指向自己锁记录的指针。如果成功获取轻量级锁。轻量级锁升级如果CAS失败说明存在竞争线程会自旋等待一定次数。如果自旋结束仍未获取到锁或者有第三个线程加入竞争立即升级为重量级锁。重量级锁运行升级后对象头指向操作系统的Monitor对象。竞争失败的线程会被操作系统挂起进入阻塞队列。当锁被释放时操作系统会唤醒所有等待线程重新竞争。面试题2synchronized和ReentrantLock有什么区别标准答案两者都是可重入锁主要区别如下维度synchronizedReentrantLock实现层面JVM原生实现由C编写JDK API实现纯Java代码锁释放自动释放异常时JVM会自动执行monitorexit必须手动在finally块中调用unlock()否则会造成死锁公平性仅支持非公平锁支持公平锁和非公平锁构造方法传入true开启公平锁高级特性不支持中断、超时获取仅支持一个条件变量wait/notify支持lockInterruptibly()中断等待、tryLock()超时获取、支持多个Condition条件变量性能JDK1.6优化后低竞争场景下与ReentrantLock相当高竞争场景下性能更稳定可控性更强选型建议优先使用synchronized语法简单不易出错JVM会持续优化当需要公平锁、可中断、超时获取或多个条件变量时使用ReentrantLock面试题3什么是偏向锁它的撤销过程是怎样的为什么需要STW标准答案偏向锁定义偏向锁是JDK1.6引入的一种锁优化针对的是锁在大多数情况下只会被同一个线程多次获取的场景。它让锁偏向于第一个获取它的线程后续该线程获取锁时无需任何CAS操作几乎没有开销。偏向锁撤销过程当有其他线程尝试获取偏向锁时会触发撤销操作JVM会首先等待全局安全点STW暂停所有正在执行的用户线程检查持有偏向锁的线程是否还在执行同步块如果线程已经退出同步块将对象头的Mark Word恢复为无锁可偏向状态然后让新线程重新偏向如果线程仍在执行同步块说明存在真正的竞争将锁升级为轻量级锁恢复所有用户线程的执行为什么需要STW偏向锁的撤销需要修改对象头的Mark Word而这个修改必须是原子的。如果不暂停所有线程可能会出现多个线程同时修改Mark Word的情况导致数据不一致。因此必须在全局安全点暂停所有线程确保没有任何线程正在访问对象头才能安全地进行修改。