【Java并发编程】锁机制:synchronized:底层实现、对象头、锁升级流程(偏向锁→轻量级锁→重量级锁)、锁优化、可重入性(附《思维导图》+《面试高频考点清单》)

简介: 本文系统梳理Java中`synchronized`锁机制:涵盖原子性、可见性、有序性三大特性;详解三种使用方式及对应锁对象;深入字节码(monitorenter/exit)、Monitor实现、对象头Mark Word状态变迁;完整解析偏向锁→轻量级锁→重量级锁的不可逆升级流程;并总结JVM锁优化(自适应自旋、锁消除、锁粗化)与常见误区。内容兼具深度与面试实用性。

思维导图

Java并发编程: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   // 重入次数
}

执行流程:

  1. 线程执行monitorenter时,检查_count是否为0
  2. 若为0,将_owner设为当前线程,_count加1
  3. 若不为0且_owner是当前线程,_count加1(可重入)
  4. 若不为0且_owner不是当前线程,线程进入_entryList阻塞
  5. 执行monitorexit时,_count减1,减到0时释放锁并唤醒_entryList中的线程

三、Java对象头详解(锁实现的基础)

在32位HotSpot虚拟机中,对象在内存中分为三部分:对象头、实例数据、对齐填充。其中对象头是synchronized锁机制的核心载体。

3.1 对象头结构(32位)

长度 内容 说明
32bit Mark Word 存储对象的哈希码、GC分代年龄、锁状态标志等
32bit Klass Pointer 指向对象所属类的元数据的指针
可选 数组长度 只有数组对象才有,占32bit

3.2 Mark Word的状态变化

Mark Word是一个动态的数据结构,会根据对象的锁状态复用存储空间,不同状态下的内容如下:

锁状态 25bit 4bit 1bit 2bit
无锁 对象哈希码 GC分代年龄 0 01
偏向锁 线程ID GC分代年龄 1 01
轻量级锁 指向栈中锁记录的指针 - - 00
重量级锁 指向monitor对象的指针 - - 10
GC标记 空 - - 11

64位Mark Word说明:

  • 线程ID占54bit,GC分代年龄占4bit,偏向锁标志1bit,锁标志2bit
  • 开启指针压缩时,Klass Pointer占32bit,否则占64bit

四、完整锁升级流程(JDK 1.6+)

JDK 1.6对synchronized进行了重大优化,引入了偏向锁和轻量级锁,使得锁不再直接升级为重量级锁,而是按照偏向锁→轻量级锁→重量级锁的顺序逐步升级,且升级后不可降级。

4.1 偏向锁(Biased Locking)

设计思想:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。因此可以让锁偏向于第一个获取它的线程,后续该线程获取锁时无需任何同步操作。

获取流程:

  1. 检查对象头Mark Word中的锁标志位是否为01,偏向锁标志是否为1
  2. 如果是可偏向状态,检查Mark Word中的线程ID是否为当前线程ID
  3. 如果是,直接进入同步块,无需任何CAS操作
  4. 如果不是,使用CAS操作将Mark Word中的线程ID替换为当前线程ID
  5. 如果CAS成功,获取偏向锁,进入同步块
  6. 如果CAS失败,说明存在竞争,触发偏向锁撤销

偏向锁撤销:

  • 偏向锁的撤销需要等待全局安全点(STW)
  • 暂停持有偏向锁的线程,检查该线程是否还在执行同步块
  • 如果线程已经退出同步块,将对象头恢复为无锁状态,然后重新偏向
  • 如果线程仍在执行同步块,升级为轻量级锁

注意:偏向锁默认是开启的,但在应用启动后几秒钟才会激活。可以通过-XX:-UseBiasedLocking关闭偏向锁。

4.2 轻量级锁(Lightweight Locking)

设计思想:当存在多个线程交替执行同步块时,使用用户态的CAS操作代替内核态的互斥量,避免线程切换的开销。

获取流程:

  1. 线程在自己的栈帧中创建一个名为锁记录(Lock Record)的空间
  2. 将对象头的Mark Word复制到锁记录中(称为Displaced Mark Word)
  3. 使用CAS操作尝试将对象头的Mark Word替换为指向当前线程锁记录的指针
  4. 如果CAS成功,获取轻量级锁,进入同步块
  5. 如果CAS失败,检查对象头的Mark Word是否指向当前线程的锁记录
  6. 如果是,说明是重入,直接进入同步块
  7. 如果不是,说明存在竞争,自旋等待一定次数
  8. 如果自旋后仍未获取到锁,升级为重量级锁

释放流程:

  1. 使用CAS操作将锁记录中的Displaced Mark Word替换回对象头
  2. 如果CAS成功,释放锁完成
  3. 如果CAS失败,说明锁已经升级为重量级锁,进入重量级锁的释放流程

4.3 重量级锁(Heavyweight Locking)

设计思想:当存在大量线程同时竞争锁时,轻量级锁的自旋会消耗大量CPU资源,此时升级为依赖操作系统互斥量的重量级锁。

执行流程:

  1. 锁升级为重量级锁后,对象头的Mark Word指向操作系统的monitor对象
  2. 竞争失败的线程会被操作系统挂起(进入阻塞状态)
  3. 当锁被释放时,操作系统会唤醒所有等待的线程,重新竞争锁
  4. 竞争失败的线程再次被挂起,如此循环

性能特点:

  • 优点:不会消耗CPU资源进行自旋
  • 缺点:线程阻塞和唤醒需要操作系统内核态和用户态的切换,开销很大

4.4 锁升级完整流程图

无锁状态
   ↓
偏向锁(单线程访问)
   ↓ 发生竞争
轻量级锁(多线程交替访问)
   ↓ 竞争加剧/自旋失败
重量级锁(多线程同时竞争)

五、JVM对synchronized的锁优化技术

5.1 自旋锁与自适应自旋

自旋锁:

  • 线程竞争锁失败时,不立即阻塞,而是循环执行几次空操作,等待锁释放
  • 避免了线程切换的开销,但会消耗CPU资源
  • 适用于锁持有时间很短的场景

自适应自旋(JDK 1.6引入):

  • 自旋次数不再固定,而是由前一次在同一个锁上的自旋时间和锁持有者的状态决定
  • 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋也很可能成功,从而允许自旋更长的时间
  • 如果对于某个锁,自旋很少成功过,那么虚拟机会直接跳过自旋过程,阻塞线程

5.2 锁消除

定义:JIT编译器在运行时,对一些代码上要求同步但实际上不可能存在共享数据竞争的锁进行消除。

判断依据:基于逃逸分析技术,如果一个对象不会逃逸出当前线程,那么这个对象的锁操作可以被安全地消除。

示例:

public String concatString(String s1, String s2, String s3) {
   
    // StringBuffer是线程安全的,append方法是synchronized的
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}
  • 上述代码中,sb对象是局部变量,不会逃逸出方法,因此JIT编译器会消除所有append方法的锁操作

5.3 锁粗化

定义:将多个连续的加锁和解锁操作合并成一个更大范围的加锁和解锁操作,减少频繁的锁操作带来的开销。

示例:

// 优化前
for (int i=0; i<100; i++) {
   
    synchronized (lock) {
   
        // 操作
    }
}

// 优化后
synchronized (lock) {
   
    for (int i=0; i<100; i++) {
   
        // 操作
    }
}

注意:锁粗化也有边界,如果循环内有耗时操作,粗化会导致锁持有时间过长,反而降低并发性。

六、synchronized的可重入性

6.1 可重入性定义

可重入性是指同一个线程已经获取了某个锁之后,再次请求获取该锁时不会被阻塞。

6.2 可重入性实现原理

  • 每个锁对象都有一个锁计数器和一个持有线程指针
  • 当线程第一次获取锁时,计数器加1,持有线程指针指向该线程
  • 当同一个线程再次获取该锁时,计数器再次加1
  • 当线程释放锁时,计数器减1
  • 当计数器减到0时,锁被完全释放,其他线程可以获取

6.3 可重入性的意义

  • 避免了死锁:如果没有可重入性,子类重写父类的同步方法并调用父类方法时会发生死锁
  • 简化了编程模型:程序员不需要担心自己已经获取的锁会导致自己阻塞

示例:

public class ReentrantExample {
   
    public synchronized void methodA() {
   
        System.out.println("执行methodA");
        methodB(); // 同一个线程再次获取锁,不会阻塞
    }

    public synchronized void methodB() {
   
        System.out.println("执行methodB");
    }
}

七、synchronized与ReentrantLock的对比

特性 synchronized ReentrantLock
实现方式 JVM层面实现 Java API层面实现
可重入性 支持 支持
公平性 非公平锁 支持公平锁和非公平锁
可中断性 不支持 支持lockInterruptibly()
超时获取 不支持 支持tryLock(long timeout)
条件变量 不支持(wait/notify) 支持多个Condition对象
自动释放 是(异常时自动释放) 否(必须手动在finally中释放)
性能 JDK 1.6+优化后与ReentrantLock相当 略高(在高竞争场景下)

八、核心考点与常见误区

8.1 核心考点

  1. 对象头Mark Word在不同锁状态下的结构
  2. 完整的锁升级流程及每个阶段的触发条件
  3. 偏向锁、轻量级锁、重量级锁的优缺点及适用场景
  4. 可重入性的实现原理
  5. JVM的各种锁优化技术

8.2 常见误区

  1. ❌ 错误:synchronized是重量级锁,性能很差
    ✅ 正确:JDK 1.6+引入了偏向锁和轻量级锁,在无竞争或低竞争场景下性能很高
  2. ❌ 错误:锁可以降级
    ✅ 正确:锁只能升级,不能降级。偏向锁可以撤销,但不是降级
  3. ❌ 错误:synchronized修饰静态方法和实例方法效果一样
    ✅ 正确:静态方法锁的是Class对象,实例方法锁的是this对象,两者互不影响
  4. ❌ 错误:自旋锁一定比阻塞锁好
    ✅ 正确:自旋锁适用于锁持有时间很短的场景,如果锁持有时间很长,自旋会浪费大量CPU资源

Java synchronized锁机制 面试版核心考点清单(可直接背诵)

一、基础必背考点

  1. synchronized三大特性:保证原子性、可见性、有序性(禁止临界区内外重排序)
  2. 三种使用方式及锁对象:
    • 实例方法:锁this(当前实例)
    • 静态方法:锁类名.class(Class对象)
    • 代码块:锁括号内指定的任意对象
  3. 字节码实现:通过monitorenter和monitorexit指令,异常时自动释放锁
  4. 可重入性实现:每个锁对象有持有线程指针和锁计数器,同一线程重入时计数器递增,释放时递减,减到0才真正释放

二、对象头核心考点(32位HotSpot)

  1. 对象头组成:Mark Word(32bit)+ Klass Pointer(32bit)+ 数组长度(仅数组对象)
  2. Mark Word状态复用表(必考):
锁状态 锁标志位 核心存储内容
无锁 01 对象哈希码+GC分代年龄
偏向锁 01(偏向位=1) 线程ID+GC分代年龄
轻量级锁 00 指向栈中锁记录的指针
重量级锁 10 指向操作系统Monitor的指针
  1. 64位差异:线程ID占54bit,开启指针压缩时Klass Pointer占32bit

三、锁升级全流程(绝对高频考点)

核心原则:锁只能升级不能降级,JDK1.6+默认开启偏向锁和轻量级锁

  1. 偏向锁(单线程无竞争)
    • 设计思想:让锁偏向第一个获取它的线程,后续获取无需CAS
    • 获取:CAS将Mark Word中的线程ID替换为当前线程ID
    • 升级触发:有其他线程竞争该锁
    • 关键:偏向锁撤销需要全局安全点(STW)
  2. 轻量级锁(多线程交替执行)
    • 设计思想:用用户态CAS代替内核态阻塞,避免线程切换开销
    • 获取:线程栈创建锁记录→复制Mark Word到锁记录→CAS将对象头指向锁记录
    • 升级触发:自旋失败(默认10次)或多个线程同时竞争
    • 关键:自旋会消耗CPU,适用于锁持有时间极短的场景
  3. 重量级锁(多线程同时竞争)
    • 设计思想:依赖操作系统互斥量,竞争失败的线程直接阻塞
    • 实现:对象头指向ObjectMonitor,包含_owner、_count、_entryList、_waiters
    • 缺点:线程阻塞和唤醒需要内核态/用户态切换,开销大

四、JVM锁优化技术

  1. 自适应自旋:自旋次数不固定,由前一次自旋成功率和锁持有者状态决定
  2. 锁消除:基于逃逸分析,消除不可能存在竞争的锁(如局部变量的锁)
  3. 锁粗化:将多个连续的加锁解锁操作合并为一个大锁,减少频繁操作开销

五、常见误区澄清

  1. ❌ synchronized是重量级锁 → ✅ 无竞争时是偏向锁,性能极高
  2. ❌ 锁可以降级 → ✅ 只能升级,偏向锁撤销≠降级
  3. ❌ 静态方法和实例方法互斥 → ✅ 锁对象不同,互不影响
  4. ❌ 自旋一定比阻塞好 → ✅ 锁持有时间长时,自旋会浪费大量CPU

3道高频面试题及标准答案

面试题1:详细描述synchronized的锁升级全过程

标准答案:
synchronized在JDK1.6后引入了分级锁机制,锁会按照偏向锁→轻量级锁→重量级锁的顺序逐步升级:

  1. 初始状态:对象刚创建时处于无锁可偏向状态,Mark Word中偏向位为1,线程ID为空。
  2. 偏向锁获取:当第一个线程访问同步块时,通过CAS操作将Mark Word中的线程ID设置为自己的ID。如果成功,该线程后续每次进入同步块都无需任何同步操作。
  3. 偏向锁撤销与升级:当有第二个线程竞争该锁时,会触发偏向锁撤销。JVM会暂停持有偏向锁的线程,检查其是否还在执行同步块:
    • 如果线程已退出同步块,将对象头恢复为无锁状态,重新偏向新线程
    • 如果线程仍在执行同步块,立即升级为轻量级锁
  4. 轻量级锁获取:线程在自己的栈帧中创建锁记录,将对象头的Mark Word复制到锁记录中,然后通过CAS尝试将对象头的Mark Word替换为指向自己锁记录的指针。如果成功,获取轻量级锁。
  5. 轻量级锁升级:如果CAS失败,说明存在竞争,线程会自旋等待一定次数。如果自旋结束仍未获取到锁,或者有第三个线程加入竞争,立即升级为重量级锁。
  6. 重量级锁运行:升级后,对象头指向操作系统的Monitor对象。竞争失败的线程会被操作系统挂起,进入阻塞队列。当锁被释放时,操作系统会唤醒所有等待线程重新竞争。

面试题2:synchronized和ReentrantLock有什么区别?

标准答案:
两者都是可重入锁,主要区别如下:

维度 synchronized ReentrantLock
实现层面 JVM原生实现,由C++编写 JDK API实现,纯Java代码
锁释放 自动释放,异常时JVM会自动执行monitorexit 必须手动在finally块中调用unlock(),否则会造成死锁
公平性 仅支持非公平锁 支持公平锁和非公平锁(构造方法传入true开启公平锁)
高级特性 不支持中断、超时获取,仅支持一个条件变量(wait/notify) 支持lockInterruptibly()中断等待、tryLock()超时获取、支持多个Condition条件变量
性能 JDK1.6+优化后,低竞争场景下与ReentrantLock相当 高竞争场景下性能更稳定,可控性更强

选型建议:

  • 优先使用synchronized,语法简单,不易出错,JVM会持续优化
  • 当需要公平锁、可中断、超时获取或多个条件变量时,使用ReentrantLock

面试题3:什么是偏向锁?它的撤销过程是怎样的?为什么需要STW?

标准答案:

  1. 偏向锁定义:偏向锁是JDK1.6引入的一种锁优化,针对的是锁在大多数情况下只会被同一个线程多次获取的场景。它让锁偏向于第一个获取它的线程,后续该线程获取锁时无需任何CAS操作,几乎没有开销。

  2. 偏向锁撤销过程:

    • 当有其他线程尝试获取偏向锁时,会触发撤销操作
    • JVM会首先等待全局安全点(STW),暂停所有正在执行的用户线程
    • 检查持有偏向锁的线程是否还在执行同步块:
      • 如果线程已经退出同步块,将对象头的Mark Word恢复为无锁可偏向状态,然后让新线程重新偏向
      • 如果线程仍在执行同步块,说明存在真正的竞争,将锁升级为轻量级锁
    • 恢复所有用户线程的执行
  3. 为什么需要STW:
    偏向锁的撤销需要修改对象头的Mark Word,而这个修改必须是原子的。如果不暂停所有线程,可能会出现多个线程同时修改Mark Word的情况,导致数据不一致。因此必须在全局安全点暂停所有线程,确保没有任何线程正在访问对象头,才能安全地进行修改。

相关文章
|
4月前
|
缓存 安全 算法
【Java基础】序列化与反序列化:Serializable接口、transient关键字、serialVersionUID作用(附《思维导图》+《面试高频考点清单》)
本文系统梳理Java序列化核心知识:涵盖`Serializable`接口原理、`transient`字段控制、`serialVersionUID`版本管理、自定义序列化(`writeObject`/`readObject`)、安全漏洞及防护(反序列化攻击、白名单)、单例保护(`readResolve`)等,并总结最佳实践与高频面试考点,助你深入理解与高效应用。
|
4月前
|
存储 并行计算 安全
【Java基础】Java 8-21新特性 :Lambda表达式、函数式接口、Stream流、Optional(附《思维导图》+《面试高频考点清单》)
本文系统梳理Java 8–21核心新特性,聚焦面试高频考点:Lambda表达式、函数式接口、Stream流(惰性求值/并行流/map与flatMap区别)、Optional空值安全处理,并涵盖JDK9–21关键演进(模块化、var、Record、虚拟线程等),辅以原理剖析与实战代码,助力高效备战。
|
4月前
|
安全 Java 数据库连接
【Java基础】反射与注解:核心原理、自定义注解、注解解析方式(附《思维导图》+《面试高频考点清单》)
Java反射与注解是框架底层核心:反射在运行时动态获取类结构(Class/Field/Method),实现IoC、ORM等;注解作为元数据标记代码,配合反射(RUNTIME)或注解处理器(SOURCE)实现声明式编程。二者结合支撑Spring、MyBatis等主流框架。
|
4月前
|
存储 安全 Java
【Java并发编程】ThreadLocal:核心原理、内存泄漏问题与解决方案、InheritableThreadLocal、TransmittableThreadLocal(附《思维导图》+《面试高频考点清单》)
本文全面解析 Java `ThreadLocal` 的权威指南,涵盖原理(ThreadLocalMap、弱引用key/强引用value)、内存泄漏根因与规避方案(必调 `remove()` + `try-finally`)、父子线程传递(InheritableThreadLocal)及线程池场景终极解法(TransmittableThreadLocal),附高频面试题与最佳实践。
|
7月前
|
缓存 Java 数据库
【Spring Boot】Spring Boot 全体系知识结构化拆解(附 Spring Boot 高频面试八股文精简版)
Spring Boot 是 Pivotal 基于 Spring 的“约定大于配置”快速开发框架,简化初始搭建与开发,无缝整合 Spring 全生态,内嵌容器、自动配置、起步依赖开箱即用,是 Java 企业级应用与微服务架构的核心基石。
1917 8
|
4月前
|
Java 测试技术 Nacos
【Spring全家桶】Spring Cloud 2023.0.x:配置中心:Nacos Config、Apollo(附《思维导图》+《面试高频考点清单》)
本文系统梳理Spring Cloud 2023.0.x(Leyton版)配置中心知识体系,涵盖Nacos与Apollo双引擎深度对比、Spring Boot 3.2+最新集成方式(`spring.config.import`)、动态刷新机制、权限审计、灰度发布等核心能力,助力微服务配置治理高效落地。
|
4月前
|
缓存 Java C++
【Java并发编程】锁机制:Lock体系:ReentrantLock、ReentrantReadWriteLock、Lock vs synchronized 区别(附《思维导图》+《面试高频考点清单》)
本文系统梳理Java Lock体系核心知识:涵盖ReentrantLock(可重入、公平/非公平、AQS实现)、ReentrantReadWriteLock(读写分离、锁降级、state拆分)及StampedLock(乐观读、缓解写饥饿),深度对比synchronized与Lock在实现、特性、性能及场景上的八大区别,助力高并发编程与面试通关。
|
6月前
|
存储 算法 数据挖掘
【数据库】向量数据库:核心原理、主流产品(Milvus、Pinecone)、索引类型(IVF、HNSW)、RAG中的应用
本文系统构建向量数据库完整知识体系:从基础定义、核心原理(ANN检索、存算分离架构)、主流索引(IVF/HNSW深度对比)、主流产品(Milvus/Pinecone等选型指南),到RAG落地实践与前沿趋势,兼顾理论深度与工程实战,助力高效构建企业级语义检索系统。
|
6月前
|
消息中间件 监控 Kafka
【消息队列MQ】消息丢失:全链路原因、解决方案、消息可靠性保证
消息队列MQ全链路防丢失体系:覆盖生产→Broker→消费三阶段,直击6大关键节点风险;涵盖确认机制、同步刷盘、主从复制、手动提交Offset、事务消息、死信兜底等核心方案,兼顾可靠性与性能折中。
|
4月前
|
Arthas 缓存 安全
【Java并发编程】高频实战:死锁排查、线程安全问题定位、线程dump分析(附《思维导图》+《面试高频考点清单》)
本文系统梳理Java并发编程高频实战知识:涵盖死锁排查(Coffman四条件、jstack/Arthas分析)、线程安全定位(竞态/可见性/有序性问题及原子类、Lock、ThreadLocal等方案)与线程Dump深度解析(状态识别、死锁/锁竞争/死循环模式)。