当前位置: 首页 > news >正文

并发编程——05 并发锁机制之深入理解synchronized

1 i++/i--引起的线程安全问题

1.1 问题

  • 思考:两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

    public class SyncDemo {private static int counter = 0;public static void increment() {counter++;}public static void decrement() {counter--;}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {increment();}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {decrement();}}, "t2");t1.start();t2.start();t1.join();t2.join();// 思考: counter=?log.info("{}", counter);}
    }
    
    • 以上的结果可能是正数、负数或者零。为什么?因为 Java 中对静态变量的自增和自减并不是原子操作。

1.2 原因

  • 我们可以查看i++i--i就是上面代码中的counter,为静态变量)的 JVM 字节码指令;

  • i++的 JVM 字节码指令

    getstatic i // 获取静态变量i的值,并将其值压入栈顶
    iconst_1 // 将int型常量1压入栈顶
    iadd // 将栈顶两int型数值相加并将结果压入栈顶
    putstatic i // 将结果赋值给静态变量i
    
  • i--的 JVM 字节码指令

    getstatic i // 获取静态变量i的值,并将其值压入栈顶 
    iconst_1 // 将int型常量1压入栈顶
    isub // 将栈顶两int型数值相减并将结果压入栈顶 
    putstatic i // 将结果赋值给静态变量i
    
  • 如果是单线程环境,那么上面的这 8 行指令是顺序执行(不会交错)的,最后的运行结果就可能是 0 。但是在多线程环境下,这这 8 行代码可能是交错运行,如下图:

    • 问题就出在多个线程访问共享资源,在多个线程对共享资源进行读写操作时会发生指令交错,就会出现问题;

    在这里插入图片描述

1.3 解决

  • 一段代码块内如果存在对共享资源的多线程读写操作,就称这段代码块临界区,称其操作的共享资源临界资源

  • 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

    //临界资源
    private static int counter = 0;public static void increment() { //临界区counter++;
    }public static void decrement() {//临界区counter--;
    }
    
  • 有多种手段可以避免临界区的竞态条件发生:

    • 阻塞式的解决方案:synchronizedLock
    • 非阻塞式的解决方案:原子变量
  • 虽然 Java 中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:

    • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码;
    • 同步是由于线程执行的先后顺序不同,需要一个线程等待其它线程运行到某个点后它才能运行。

2 synchronized

2.1 简介

  • synchronized同步块是 Java 提供的一种原子性内置锁(Java 内置的、使用者看不到的锁被称为内置锁,也叫作监视器锁),Java 中的每个对象都可以把它当作一个同步锁来使用。

2.2 加锁方式

在这里插入图片描述

  • 方法分类(直接修饰方法)

    • 实例方法加锁

      // 锁的是当前实例对象,多个线程用同一个实例调用此方法,需竞争这把锁;不同实例调用,互不影响
      public synchronized void method() { // 线程安全的业务逻辑
      }
      
      • 被锁对象:类的实例对象this 指向的对象,每个实例对象锁独立);

      • 比如创建A a1 = new A(); A a2 = new A(); ,线程 1 调用a1.method()、线程 2 调用a2.method(),因为锁的是不同实例,二者可同时执行方法;

    • 静态方法加锁

      // 锁的是当前类的 Class 对象,不管创建多少实例,调用此静态方法都竞争同一把锁
      public static synchronized void method1() { // 线程安全的业务逻辑
      }
      
      • 被锁对象:类的Class 对象(每个类在 JVM 中只有一个 Class 对象,全局唯一);
      • 比如 A a1 = new A(); A a2 = new A(); ,线程 1 调用 a1.method1() 、线程 2 调用 a2.method1() ,由于锁的是 A.class ,二者会串行执行方法;
  • 代码块分类(手动指定锁对象,更灵活)

    • 实例对象(this)加锁

      public void method() {// 锁当前实例对象(this),效果和 “实例方法加锁” 类似,只是锁范围更灵活(可缩小到代码块)synchronized (this) { // 线程安全的业务逻辑}
      }
      
      • 被锁对象:类的实例对象(和“实例方法加锁”锁的对象一致,都是当前实例);
      • 比如方法里有两段逻辑,只有部分逻辑需要线程安全,就可以用这种方式,只给关键代码加锁,减少锁竞争;
    • class 对象加锁

      public void method() {// 锁当前类的 Class 对象(比如 SynchronizedDemo.class),效果和 “静态方法加锁” 类似synchronized (SynchronizedDemo.class) { // 线程安全的业务逻辑}
      }
      
      • 被锁对象:类的Class 对象(和“静态方法加锁”锁的对象一致,全局唯一);
      • 常用于静态变量修改、静态工具方法线程安全控制,不管是不是静态方法,只要锁 Class 对象,就会和静态方法加锁竞争同一把锁;
    • 任意实例对象(Object)加锁

      // 定义一个锁对象(可以是任意类型,只要是同一个实例)
      String lock = ""; 
      public void method() {// 锁自定义的 lock 对象,多个线程竞争这同一个对象的锁synchronized (lock) { // 线程安全的业务逻辑}
      }
      
      • 被锁对象:自定义的任意实例对象(自己创建的对象,作为锁标识);
      • 这种方式最灵活,比如想让不同方法、不同类共享同一把锁,就可以把 lock 定义为公共对象;也能精准控制锁的范围(比如不同逻辑用不同锁,减少锁竞争);
  • 核心区别总结

    分类维度关键差异点
    锁的对象方法加锁(实例/静态)锁的是 “实例对象” 或 “Class 对象”;代码块加锁可灵活指定任意对象锁
    锁的粒度方法加锁是 “整个方法” ;代码块加锁可缩小到 “部分代码”,更灵活控制线程安全范围
    使用场景简单场景用方法加锁;复杂场景(需精准控制锁范围、自定义锁对象)用代码块加锁
  • 简单说,synchronized本质是通过“对象锁”保证同一时间只有一个线程执行临界区代码,不同加锁方式只是锁的对象、锁的范围不同,实际开发要根据场景选(比如想锁实例用 this 或实例方法,想全局锁用 Class 对象或静态方法,想灵活自定义锁用任意对象代码块),避免锁竞争影响性能,也得保证线程安全。

2.3 使用synchronized解决前面的共享问题

  • 方式一:

    // 同步静态方法 increment,当多个线程调用此方法时,会竞争当前类的锁(Class 对象锁)
    // 同一时刻,只有一个线程能进入该方法执行 counter++ 操作,保证线程安全
    public static synchronized void increment() {counter++;
    }public static synchronized void decrement() {counter--;
    }
    
  • 方式二:

    // 定义一个私有的、静态的字符串类型锁对象 lock,这里用空字符串只是作为一个锁的标识,实际只要是同一个对象即可
    private static String lock = "";// 非同步方法 increment,方法内通过同步代码块来实现线程安全
    public static void increment() {// 进入同步代码块,这里锁定的是 lock 对象,多个线程要执行此代码块,需先获取 lock 对象的锁synchronized (lock) {// 执行 counter 自增操作,由于有锁的保护,同一时刻只有一个线程能执行这行代码counter++; }
    }public static void decrement() {synchronized (lock) { counter--; }
    }
    
  • 在上面的两种修改方式中,synchronized实际是用对象锁保证了临界区内代码的原子性:

    在这里插入图片描述

2.4 底层实现原理分析

  • synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低。

2.4.1 查看synchronized的字节码指令序列

  • 同步方法(synchronized 修饰方法)

    • 方法的访问标志(Access flags)里,会有一个 ACC_SYNCHRONIZED 标志(值为 0x0020 );
    • 原理:JVM 执行方法时,会检查 ACC_SYNCHRONIZED 标志。如果有,会先获取监视器锁(monitor),执行完方法后再释放锁。同一时间,只有一个线程能持有这把锁,保证线程安全;

    Java 方法的 access_flags(访问标志),是用 多个标志位 “按位或(|)” 拼接 出来的;

    比如:

    • ACC_PUBLIC0x0001)代表方法是 public
    • ACC_STATIC0x0008)代表方法是 static
    • ACC_SYNCHRONIZED0x0020)代表方法是 synchronized

    当一个方法同时是 publicstaticsynchronized 时,它的 access_flags 就是这三个标志的按位或结果

    0x0001(ACC_PUBLIC) 
    | 0x0008(ACC_STATIC) 
    | 0x0020(ACC_SYNCHRONIZED) 
    = 0x0029 
    

    在这里插入图片描述

    在这里插入图片描述

  • 同步代码块(synchronized (lock) { ... }

    • 字节码中会出现 monitorentermonitorexit 指令:
      • monitorenter:进入同步代码块时,尝试获取 lock 对象的监视器锁。获取成功后,锁的计数器 +1(重入性体现);
      • monitorexit:退出同步代码块时,释放锁,锁的计数器 -1。当计数器归 0,其他线程才能获取锁;

    在这里插入图片描述

  • 监视器锁(monitor)是什么?

    • 可以理解为**“锁的底层实现”**,每个对象在 JVM 中都有一个对应的 monitor(可看作一种数据结构);
    • 当线程获取锁时,实际是获取 monitor 的所有权;释放锁时,释放 monitor
    • 作用:保证同一时间只有一个线程执行临界区代码,解决线程安全问题;
  • 关键区别(同步方法 vs 同步代码块)

    对比项同步方法(ACC_SYNCHRONIZED同步代码块(monitorenter/monitorexit
    实现方式靠方法的 ACC_SYNCHRONIZED 标志靠显式的 monitorenter/monitorexit 指令
    锁的粒度整个方法都受锁保护仅代码块范围受锁保护(更灵活,可缩小锁范围)
    重入性支持支持(JVM 自动处理锁的重入计数)支持(monitorenter 指令会检查当前线程是否持有锁,自动重入)

2.4.2 重量级锁实现之 Monitor(管程/监视器)机制详解

2.4.2.1 Monitor 简介
  • Monitor,直译为“监视器”,而操作系统领域一般翻译为管程(管理共享资源的“容器”);

  • 管程本质上是一种并发编程的基础模型,用来管理共享变量,以及对共享变量的操作过程,让这些操作能安全地支持多线程并发。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发;

  • 作用:

    • 解决多线程并发问题:多线程直接操作共享变量,容易出现线程安全问题(比如数据混乱、结果不一致)。Monitor 的作用就是给共享变量和操作加一层保护,让同一时间只有一个线程能执行关键操作,避免混乱;
    • Java 并发的基石
      • Java 1.5 之前,唯一的并发工具就是管程(靠 synchronized + wait/notify 实现);
      • Java 1.5 之后的并发包(java.util.concurrent),底层也是基于管程思想封装的(比如 ReentrantLock 等锁,本质和管程同源);
  • Monitor 在 Java 里的体现

    • synchronized 强绑定

      • Java 里,每个对象(Object)在 JVM 中都对应一个 Monitor 监视器

      • 当用 synchronized 修饰方法/代码块时,本质就是 关联到对象的 Monitor,获取 Monitor 的锁

    • wait/notify/notifyAll是管程的“操作接口”:这三个方法是 Object 类的方法(每个对象都有),它们的作用就是操作 Monitor 的状态

      • wait():释放当前持有的 Monitor 锁,进入等待队列,等待被唤醒;
      • notify()/notifyAll():唤醒等待队列中的线程,让它们重新竞争 Monitor 锁;
  • JVM 规范里的 Monitor

    • 《Java 语言规范》

      在这里插入图片描述

      • 每个对象都会关联一个 Monitor,用来控制多线程对对象状态(共享变量)的并发访问;

      • 不管是 synchronized 方法,还是 synchronized 代码块,最终都依赖 Monitor 实现同步

  • 《Java 虚拟机规范》:JVM 靠 “Monitor 这一种同步结构”,同时支持两种同步场景:

    在这里插入图片描述

    • 修饰整个方法(synchronized 方法);
    • 修饰方法内的一段代码(synchronized 代码块)。
2.4.2.2 MESA模型分析
  • 管程发展出过 Hasen 模型、Hoare 模型、MESA 模型,目前广泛使用的是 MESA 模型(Java 的 synchronized 就是参考它实现的);

    在这里插入图片描述

    • 共享变量:需要被多线程安全访问的变量(比如上图里的“共享变量 V”);

    • 入口等待队列:多线程要进入管程操作共享变量时,先在这里排队。同一时间,只有一个线程能拿到锁,进入管程执行

    • 条件变量 + 等待队列

      • 条件变量(比如“条件变量 A/B”)是管程里的同步工具,用来解决线程之间的复杂同步问题(比如线程需要等待某个条件满足才能继续执行);
      • 每个条件变量对应一个等待队列,线程调用 wait() 时,会进入对应条件变量的等待队列,释放管程锁,等待被唤醒;
  • Java 的 synchronized 参考了 MESA 模型,但做了简化:

    在这里插入图片描述

    • 条件变量只有 1 个:Java 里,管程(synchronized)的条件变量是通过 Objectwait/notify 实现的,整个管程只有一个条件变量队列(对比 MESA 模型可以有多个条件变量);

    • 使用更简单:虽然条件变量少,但足够解决大部分线程同步问题(如果需要复杂条件,可通过手动逻辑模拟多个条件变量);

  • 示例代码:用 synchronized + wait/notifyAll 演示了管程的同步逻辑,对应 MESA 模型的执行流程

    public class WaitDemo {// 管程的“锁对象”,对应 MESA 模型的“管程锁”final static Object obj = new Object(); public static void main(String[] args) throws InterruptedException {// 线程 t1:获取 obj 锁,执行 wait(),进入条件变量队列new Thread(() -> {synchronized (obj) {// 操作共享资源try {obj.wait(); // 释放锁,进入条件变量队列} catch (InterruptedException e) { ... }}}).start();// 线程 t2:和 t1 类似,也会执行 wait(),进入条件变量队列new Thread(() -> {synchronized (obj) {// 操作共享资源try {obj.wait(); } catch (InterruptedException e) { ... }}}).start();// 主线程:等待 2 秒后,调用 notifyAll() 唤醒所有等待线程Thread.sleep(2000);synchronized (obj) {obj.notifyAll(); // 唤醒条件变量队列里的所有线程}}
    }
    
    • 线程进入“入口等待队列”:线程 t1t2 执行 synchronized (obj) 时,会先竞争 obj 的锁。如果锁被占用,就进入**“入口等待队列”**排队;

    • 线程执行 wait(),进入“条件变量队列”:线程拿到锁后,执行 obj.wait()

      • 释放 obj 的锁(让其他线程可以进入管程);
      • 进入 obj 对应的条件变量等待队列(对应 MESA 模型里的“条件变量队列”);
    • 主线程调用 notifyAll(),唤醒等待线程:主线程执行 obj.notifyAll() 时:

      • 唤醒 obj 条件变量队列里的所有线程t1t2 都会被唤醒);

      • 这些线程被唤醒后,会重新进入**“入口等待队列”**,重新竞争 obj 的锁;

      • 拿到锁的线程,继续执行 wait() 之后的逻辑。

2.4.2.3 ObjectMonitor数据结构分析
  • java.lang.Object类定义了wait()notify()notifyAll()方法,这些方法的具体实现,依赖于 JVM 中的ObjectMonitor数据结构;

  • 核心字段如下(在 Hotspot 源码ObjectMonitor.hpp中):

    ObjectMonitor() {// 1. 对象头相关_header       = NULL;    // 对象头(和 Java 对象的 markOop 关联,存哈希、分代年龄、偏向锁标记等)// 2. 锁的基础状态_count        = 0;       // 记录 Monitor 的一些统计状态(比如锁的竞争次数,JVM 内部调试用)_waiters      = 0;       // 等待线程的总数(调用 wait() 的线程数,辅助统计)_recursions   = 0;       // 锁的重入次数(同一线程多次加锁时,记录重入深度)// 3. 关联 Java 对象_object       = NULL;    // 指向关联的 Java 对象(Monitor 和 Java 对象绑定,每个对象对应一个 Monitor)// 4. 持有锁的线程_owner        = NULL;    // 当前持有锁的线程(关键!标记谁在占用 Monitor,竞争锁的核心)// 5. 等待队列(wait() 相关)_WaitSet      = NULL;    // 等待队列的头节点(调用 wait() 的线程,会被链到这个双向循环链表)_WaitSetLock  = 0 ;      // 保护 _WaitSet 队列的锁(操作等待队列时,防止多线程混乱)// 6. 竞争锁的辅助队列_cxq          = NULL;    // 竞争锁的临时队列(多线程竞争时,先临时存在这个单向链表,FILO 结构)// 7. 阻塞线程队列(EntryList)_EntryList    = NULL;    // 竞争锁失败的线程队列(被阻塞的线程,等待重新竞争锁)// 8. 其他辅助字段(JVM 调试、优化用,实际开发少关注)_Responsible  = NULL;    // 负责唤醒的线程(JVM 内部用于锁竞争的优化,标记“该由谁唤醒”)_succ         = NULL;    // 后继线程(锁竞争的辅助标记,记录下一个该竞争的线程)FreeNext      = NULL;    // 空闲 Monitor 链表(JVM 管理 Monitor 资源的辅助字段)_SpinFreq     = 0 ;      // 自旋次数统计(自适应自旋锁的优化,记录自旋尝试次数)_SpinClock    = 0 ;      // 自旋时间统计(自适应自旋锁的优化,记录自旋耗时)OwnerIsThread = 0 ;      // 标记 _owner 是否是线程(防止其他对象误判,JVM 内部兼容用)_previous_owner_tid = 0; // 前一个持有锁的线程 ID(JVM 内部调试、追踪锁竞争用)
    }
    
2.4.2.4 synchronized重量级锁实现原理
  • synchronized 底层靠 monitor 对象 + 队列(cxq/EntryList/WaitSet) + 操作系统互斥锁(mutex) 实现。由于线程阻塞/唤醒需要 用户态 ↔ 内核态切换,开销高,因此被称为“重量级锁”;

  • 核心组件:

    组件作用
    monitor管理锁的核心对象,内置队列(cxqEntryListWaitSet),控制线程竞争与同步
    cxq竞争锁的临时单向队列(基于 CAS 实现,快速暂存竞争失败的线程)
    EntryList竞争锁的阻塞双向队列(存放等待重新竞争锁的线程)
    WaitSet条件等待队列(调用wait()的线程会进入这里,等待被 notify 唤醒)
    mutex(互斥锁)操作系统提供的底层锁,monitor依赖它实现线程的阻塞/唤醒(涉及内核态切换)
  • 线程竞争锁流程(获取锁)

    1. 线程竞争:多个线程同时竞争锁时,先尝试 CAS 抢占;
    2. 进入 cxq:竞争失败的线程,先进入 cxq(单向队列,基于 CAS 快速暂存);
    3. 迁移到 EntryList:JVM 会“按需”把 cxq 中的线程迁移到 EntryList(双向队列,降低尾部竞争);
    4. 再次竞争锁EntryList 中的线程,等待锁释放后,重新竞争锁(可能直接从 cxq 抢锁,取决于策略);
  • 线程释放锁流程(释放锁)

    • 释放锁:持有锁的线程执行完同步代码,或调用 wait() 时,释放 monitor 的锁;

    • 唤醒策略(默认策略是 Qmode=0):

      • 如果 EntryList 为空,把 cxq 的线程按顺序迁移到 EntryList,唤醒第一个线程(后来的线程可能先获取锁,类似“后来者优先”);

      • 如果 EntryList 不为空,直接唤醒 EntryList 中的线程(先来者优先);

        假设cxq里线程的排队顺序(从队列头部到尾部,即“入队先后顺序” )是**A → B → C**(A 最先进入cxq队列,C最后进入),此时EntryList为空;

        那么cxq的线程会按原顺序(保持A → B → C的先后顺序)迁移到EntryList,所以迁移后EntryList里的线程顺序也是**A → B → C**(A 成为EntryList的第一个元素);

        迁移完成后,会唤醒EntryList里的第一个线程,也就是**A** 。后续A会去竞争锁,拿到锁后执行同步代码逻辑。

    • 处理 WaitSet:如果调用 notify()/notifyAll(),会从 WaitSet 唤醒线程,移到 EntryList 重新竞争;

  • wait() / notify() 流程

    • wait():持有锁的线程调用 wait() → 释放锁 → 进入 WaitSet 等待 → 被唤醒后移到 EntryList 重新竞争;

    • notify():持有锁的线程调用 notify() → 从 WaitSet 选一个线程(或全部,notifyAll())→ 移到 EntryList → 线程重新竞争锁;

    在这里插入图片描述

  • synchronized被称为“重量级锁”的原因:线程的阻塞和唤醒依赖操作系统的 mutex,需要从用户态切换到内核态(内核态负责调度线程阻塞/唤醒);

    • 用户态 → 内核态切换:涉及 CPU 权限切换、上下文保存/恢复,开销非常高

    • 因此,synchronized 重量级锁的“重”,本质是内核态切换的高成本

  • 为什么需要 cxqEntryList 两个队列?

    • cxq(单向队列):基于 CAS 实现,支持线程“无锁竞争”快速入队,适合高并发抢锁场景(减少锁竞争的直接冲突);

    • EntryList(双向队列):用于存放真正等待唤醒的线程,每次唤醒时迁移 cxq 的线程到这里,降低 cxq 的尾部竞争(双向链表更适合批量迁移、唤醒操作);

    • 协同作用cxq 负责“快速暂存”竞争失败的线程,EntryList 负责“有序管理”等待唤醒的线程,分工减少锁竞争的压力。

2.5 重量级锁的优化策略

  • 早期synchronized是“重量级锁”,依赖操作系统mutex,涉及用户态/内核态切换,开销大。JVM 内置锁在 JDK 1.5 后引入多种优化(锁粗化、锁消除、轻量级锁、偏向锁、自适应自旋),让synchronized性能大幅提升,甚至能和java.util.concurrent.Lock媲美。

2.5.1 锁粗化(Lock Coarsening)

  • 核心思想:将多个连续的小锁,合并成一个大锁,减少“加锁-解锁”的次数,降低开销。也就是说,如果 JVM 检测到有连续的对同一对象的加锁、解锁操作,就会把这些加锁、解锁操作合并为对这段区域进行一次连续的加锁和解锁;

  • 例:

    • 原始代码(多个小同步块):

      synchronized (lock) {// 代码块 1
      }
      // 无关代码
      synchronized (lock) {// 代码块 2
      }
      
    • JVM 优化后(合并成一个大同步块):

      synchronized (lock) {// 代码块 1// 无关代码// 代码块 2
      }
      
    • 效果:加锁/解锁次数从 2 次 减少到 1 次,降低了开销;

  • 为什么需要锁粗化?每次“加锁”和“解锁”都可能涉及线程切换、内核态调用,有性能开销。如果代码中有大量连续的小同步块(对同一个对象加锁),频繁加锁/解锁会累积成显著开销;

  • 例:StringBufferappend 方法是线程安全的(内部用 synchronized 加锁)

    • 原始代码(多次调用 append,隐含多次加锁/解锁):

      StringBuffer buffer = new StringBuffer();
      buffer.append("aaa").append("bbb").append("ccc");
      
    • JVM 优化:JVM 检测到“连续对 buffer 加锁”,会合并成一次加锁、一次解锁

      • 第一次 append 时加锁,最后一次 append 结束后解锁;
      • 中间的 append 无需重复加锁/解锁,减少开销;
  • 锁粗化的“有效性”原理

    • 减少“加锁-解锁”的开销

      • 每次加锁/解锁可能触发线程阻塞、内核态切换,成本很高;
      • 合并后,次数从 N 次 → 1 次,直接减少这部分开销;
    • 对开发者的启示

      • 不要手动写“零碎的同步块”:如果逻辑上可以合并,JVM 会帮你优化,但代码写得越简洁(减少不必要的小同步块),越容易触发锁粗化;
      • 例如:避免在循环里反复加锁同一个对象,尽量把锁的范围扩大(只要不影响线程安全)。

2.5.2 锁消除(Lock Elimination)

  • 什么是锁消除?JVM 通过逃逸分析(Escape Analysis)发现:某些加锁的共享数据实际上不会被多线程访问(仅在一个线程内使用),于是自动消除这些不必要的锁,避免加锁/解锁的性能开销;

  • 为什么需要锁消除?

    • 加锁/解锁本身有开销(线程切换、内核态调用等);

    • 如果数据确定不会被多线程竞争,加锁就是“多余操作”,反而降低性能;

    • 锁消除能自动识别并移除这些无用锁,提升程序效率;

  • 锁消除的实现基础:逃逸分析

    • 逃逸分析的作用。判断一个对象的作用范围:是否会“逃逸”出当前方法/线程,被其他线程访问;

    • 锁消除的判断条件。如果 JVM 通过逃逸分析发现:

      • 对象是局部变量(作用域仅限当前方法);

      • 对象无法被其他线程访问(没有逃逸出当前线程);

    • 那么,对这个对象的加锁操作(比如 StringBuffer.appendsynchronized )会被 自动消除

  • 在代码层面上,我们无法直接控制 JVM 进行锁消除优化,这是由 JVM 的 JIT 编译器在运行时动态完成的。但我们可以通过编写高质量的代码,使 JIT 编译器更容易识别出可以进行锁消除的场景。例如:

    public class LockEliminationTest {public void append(String str1, String str2) {// StringBuffer 是局部变量,作用域仅限 append 方法StringBuffer stringBuffer = new StringBuffer(); // append 是同步方法(内部有 synchronized)stringBuffer.append(str1).append(str2); }public static void main(String[] args) {LockEliminationTest demo = new LockEliminationTest();long start = System.currentTimeMillis();// 循环调用 append,触发 JIT 优化for (int i = 0; i < 10000000; i++) { demo.append("aaa", "bbb");}System.out.println("执行时间: " + (System.currentTimeMillis() - start) + " ms");}
    }
    
    • StringBuffer局部变量,作用域仅限 append 方法 → 不会逃逸到其他线程;

    • JVM 通过逃逸分析识别到这一点 → 消除 append 内部的 synchronized 锁;

    • 测试结果:

      • 关闭锁消除-XX:-EliminateLocks):每次 append 都要加锁/解锁 → 执行时间长(比如 4688 ms);

      • 开启锁消除-XX:+EliminateLocks,JDK8+ 默认开启):JVM 自动移除无用锁 → 执行时间短(比如 2601 ms);

  • 锁消除的价值

    • 性能提升:避免不必要的加锁/解锁开销,尤其对高频调用的方法(比如示例中的循环调用),优化效果显著;

    • 开发启示

      • 无需手动控制锁消除(由 JVM 自动完成);

      • 写代码时,尽量使用局部变量(减少对象逃逸),让 JVM 更容易识别可优化的锁。

2.5.3 CAS自旋优化(Spinlock Optimization)

  • 核心思想:让线程在竞争锁失败时,不直接阻塞,而是自旋等待一段时间(空转 CPU),期望持有锁的线程能快速释放锁,从而避免线程阻塞的高开销;

  • 为什么需要自旋优化?

    • 重量级锁的痛点:线程竞争锁失败时,会进入阻塞状态 → 触发用户态 ↔ 内核态切换(非常耗时);

    • 自旋的价值:如果持有锁的线程很快释放锁(比如同步块执行时间短),竞争线程通过“自旋等待”拿到锁,就能避免阻塞的高开销

  • 自旋优化的适用场景

    • 多核 CPU 更有效

      • 单核 CPU 自旋会浪费 CPU 时间(没有其他线程能释放锁);

      • 多核 CPU 中,其他核的线程可能快速释放锁,自旋等待才有意义;

    • 配合短同步块:如果同步块执行时间很短(比如几纳秒),自旋等待的性价比高;如果同步块执行时间长,自旋会浪费 CPU,不如直接阻塞;

  • 自旋优化的发展(JVM 版本演进)

    • Java 6 及之前:自适应自旋

      • 手动控制

        • 通过参数开启:-XX:+UseSpinning(默认开启);

        • 设置自旋次数:-XX:PreBlockSpin(比如设为 10,代表自旋 10 次);

      • 自适应逻辑:JVM 会根据历史自旋成功率动态调整自旋次数

        • 如果上次自旋成功(拿到锁),认为这次也容易成功 → 增加自旋次数;
        • 如果上次自旋失败,减少自旋次数甚至不自旋;
    • Java 7 及之后:完全自适应

      • 自动控制:移除 PreBlockSpin 等参数,自旋次数由 JVM 完全自动调整(根据当前线程竞争、锁释放的历史数据动态决策);

      • 强制自旋:只要触发锁竞争,JVM 会先自旋尝试,不再允许手动关闭(因为 JVM 认为自旋的收益大于成本);

  • 自旋优化的本质目标:减少线程阻塞-唤醒的次数

    • 阻塞/唤醒涉及用户态 ↔ 内核态切换,是重量级锁最大的开销;

    • 自旋通过空转 CPU 等待,让线程尽量保持在用户态,避免进入内核态阻塞,从而提升性能。

2.5.4 轻量级锁(Lightweight Locking)

  • 思考一个这个场景:当多个线程交替获取同一把锁,且竞争不激烈

    • 比如线程 A 用完锁释放后,线程 B 才来获取,不会出现同时争抢;

    • 这种场景下,不需要直接升级到重量级锁(避免内核态切换的高开销);

  • 对于上面这种场景,就可以引入轻量级锁,轻量级锁的作用就是:避免直接使用 monitor(重量级锁),减少系统调用和线程阻塞的开销

    • 重量级锁依赖 ObjectMonitor,涉及内核态切换(用户态 ↔ 内核态),成本高;

    • 轻量级锁通过 CAS(Compare-And-Swap) 实现,全程在用户态完成,开销低;

  • 轻量级锁的核心逻辑(对比重量级锁讲解)

    • 加锁流程

      • 轻量级锁:线程在栈中创建“锁记录(Lock Record)”,用 CAS 操作 尝试将对象头的“标记”指向自己的锁记录;
        • 成功:加锁完成(用户态操作,无内核调用);
        • 失败:说明有竞争,直接升级为重量级锁(不再自旋,避免无效等待);
  • 重量级锁:依赖 ObjectMonitor,竞争失败会进入阻塞队列(涉及内核态切换);

    • 与“自旋”的关系

      • 错误理解:认为“轻量级锁竞争失败会自旋,自旋多次后升级为重量级锁”;

      • 正确逻辑:轻量级锁没有自旋逻辑!竞争失败时,直接升级为重量级锁,避免浪费 CPU。自旋是重量级锁的优化策略,用于竞争失败后的最后尝试;

  • 轻量级锁的价值

    • 针对“交替获取锁”优化:适合线程交替使用锁的场景(比如线程 A 释放后,线程 B 才来获取),用 CAS 快速加锁,避免重量级锁的内核态开销;

    • 与重量级锁分工明确,二者配合,覆盖不同并发强度的需求:

      • 轻量级锁:处理竞争不激烈、交替获取 的场景;

      • 重量级锁:处理竞争激烈、需要阻塞等待 的场景。

2.5.5 偏向锁(Biased Locking)

  • 思考一个这个场景:当锁自始至终(或很长时间)只有一个线程访问

    • 比如某个同步方法,从始至终只有线程 A 调用,没有其他线程竞争;

    • 这种场景下,频繁的 CAS 操作(轻量级锁的加锁方式)会带来不必要的开销;

  • 对于上面这种场景,就可以引入偏向锁,偏向锁的作用就是:消除无竞争场景下的 CAS 开销,让单线程加锁/解锁更快

    • 轻量级锁依赖 CAS 操作(虽然在用户态,但仍有一定开销);

    • 偏向锁通过“标记锁的偏向线程”,让后续加锁无需 CAS,直接复用;

  • 偏向锁的核心逻辑

    • 加锁流程

      • 首次加锁:JVM 检测到锁对象未被偏向 → 用 CAS 操作将对象头的“偏向线程 ID”标记为当前线程。这一步需要 CAS,但只在首次加锁时执行;
  • 后续加锁:线程再次访问锁时,无需 CAS。直接检查对象头的“偏向线程 ID”是否是自己 → 是则直接获取锁,无需任何操作;

    • 对比轻量级锁

      锁类型适用场景加锁核心操作优势
      偏向锁单线程重复加锁首次 CAS 标记线程,后续直接复用消除无竞争下的 CAS 开销
      轻量级锁多线程交替加锁(低竞争)每次加锁都要 CAS避免重量级锁的内核态切换
  • 偏向锁的价值

    • 针对单线程加锁优化。适合长期被一个线程持有、无竞争的锁,比如工具类的同步方法,只有主线程调用 → 偏向锁能让加锁开销趋近于 0;

    • 消除重入开销。对于重入锁(同一线程多次进入同步块),偏向锁标记后,无需每次 CAS 或检查 Monitor → 直接复用,消除重入带来的 CAS 开销。

2.5.6 锁升级的过程

在这里插入图片描述

  • 注意:synchronized事实上是先有的重量级锁的实现,然后根据实际分析优化实现了偏向锁和轻量级锁。

2.6 synchronized锁升级详解

  • 思考:synchronized加锁加在对象上,那么对象是如何记录锁状态的(如何判断是否加锁成功、锁状态记录在哪儿)?

2.6.1 sychronized多种锁状态设计详解

2.6.1.1 对象的内存布局
  • 在 HotSpot 中,对象在内存中的存储布局可以分为 3 部分

    区域作用
    对象头(Header)存储对象的元数据(如锁状态、哈希码、GC 分代年龄等),是锁升级的核心
    实例数据(Instance Data)存储对象的属性数据(如类的字段值)
    对齐填充(Padding)保证对象起始地址是 8 字节的整数倍(HotSpot 要求),无关业务逻辑

    在这里插入图片描述

2.6.1.2 对象头详解
  • HotSpot 虚拟机中存储的对象的对象头的关键部分是 Mark Word

    • 它存储了对象的运行时元数据,包括:

      • 哈希码(HashCode)、GC 分代年龄(用于垃圾回收);

      • 锁状态标志(偏向锁、轻量级锁、重量级锁的状态);

      • 线程持有的锁信息(偏向线程 ID、轻量级锁的指向等);

    • Mark Word 的长度:32 位虚拟机中占 32bit,64 位虚拟机中占 64bit(开启指针压缩后,实际存储会更紧凑);

  • Klass Pointer(类型指针)

    • 指向对象所属类的元数据(即 Class 对象),JVM 通过它确定对象的类型;

    • 长度:

      • 32 位虚拟机中占 4 字节;
      • 64 位虚拟机中,默认开启指针压缩(UseCompressedOops),占 4 字节;关闭后占 8 字节;
  • 数组长度(仅数组对象有):如果对象是数组(如 int[]Object[] ),对象头中会额外存储数组长度(4 字节);

    在这里插入图片描述

2.6.1.3 使用JOL工具查看内存布局
  • JOL(Java Object Layout)是一个 查看 Java 对象内存布局的工具,作用:

    • 分析对象的“对象头、实例数据、对齐填充”具体占多少字节;

    • 验证“锁状态存在对象头的 Mark Word 中”这一底层逻辑;

  • 引入依赖(Maven)

    <!-- 查看Java对象布局、大小工具 -->
    <dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.18</version>
    </dependency>
    
  • 使用示例:利用 JOL 查看64位系统 Java 对象(空对象)

    import org.openjdk.jol.info.ClassLayout;public class JOLTest {public static void main(String[] args) throws InterruptedException {Object obj = new Object();// 打印对象的内存布局System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
    }
    
  • 默认开启指针压缩,输出结果解析(以 64 位系统为例):

    • 对象头占 12 字节(Mark Word 8 字节 + Klass Pointer 4 字节);

    • 加上对齐填充,空对象(new Object())总大小是 16 字节

    在这里插入图片描述

    OFFSET:字段的偏移地址(从 0 开始);

    SIZE:字段占用的字节数;

    TYPE DESCRIPTION:字段类型描述(object header 是对象头);

    VALUE:内存中存储的实际值(对象头的 Mark Word 内容);

  • 关闭指针压缩后,输出结果解析:

    • 对象头占 16 字节(Mark Word 8 字节 + Klass Pointer 8 字节);

    • 空对象总大小仍为 16 字节(无对齐填充);

    在这里插入图片描述

  • 练习:下面例子中 obj 对象占多少个字节?12 + 8 + 4 = 24 字节

    public class ObjectTest {public static void main(String[] args) {Object obj = new Test();System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
    }class Test {private long p; // long 占 8 字节
    }
    
    • 对象头:12 字节(Mark Word 8 + Klass Pointer 4);
    • 实例数据long p 占 8 字节;
    • 对齐填充:4 字节(保证总大小是 8 的倍数,24 是 8 的倍数);
  • 回到之前的问题:synchronized加锁加在对象上,那么对象是如何记录锁状态的?

    • 通过 JOL 可以验证:锁状态(偏向锁、轻量级锁、重量级锁)记录在对象头的 Mark Word 中

      • Mark Word 存储哈希码、GC 分代年龄、锁状态标志、偏向线程 ID 等信息;

      • synchronized 加锁时,JVM 会修改 Mark Word 的锁状态标志,实现锁升级。

2.6.1.4 Mark Word 是如何记录锁状态的?
  • Hotspot 通过markOop类型实现 Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储很多的运行时数据(哈希码、锁状态、分代年龄等),考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态(无锁、偏向锁、轻量级锁等)复用自己的存储空间;

  • Mark Word 的锁标记结构

    //  32 bits:
    //  --------
    //             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
    //             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
    //             size:32 ------------------------------------------>| (CMS free block)
    //             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
    //
    //  64 bits:
    //  --------
    //  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
    //  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
    //  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
    //  size:64 ----------------------------------------------------->| (CMS free block)
    //
    //  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
    //  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
    //  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
    //  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)。。。。。。
    //    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
    //    [0           | epoch | age | 1 | 01]       lock is anonymously biased
    //
    //  - the two lock bits are used to describe three states: locked/unlocked and monitor.
    //
    //    [ptr             | 00]  locked             ptr points to real header on stack
    //    [header      | 0 | 01]  unlocked           regular object header
    //    [ptr             | 10]  monitor            inflated lock (header is wapped out)
    //    [ptr             | 11]  marked             used by markSweep to mark an object
    //                                               not valid at any other time
    
  • 32 位 JVM 下的对象结构描述

    在这里插入图片描述

  • 64 位 JVM 下的对象结构描述

    在这里插入图片描述

    • 哈希码(hash):对象的唯一标识,延迟计算(调用 System.identityHashCode 时才生成);
  • 分代年龄(age):保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代;

    • 偏向锁标识位(biased_lock):由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位;
  • 锁标志位(lock):区分锁状态(01=无锁/偏向锁,00=轻量级锁,10=重量级锁);

    • 线程 ID(JavaThread):保存持有偏向锁的线程ID。当使用偏向锁且某个线程持有锁对象的时候,该锁对象就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作,避免频繁 CAS。这个线程ID并不是JVM分配的线程ID号,和 Java Thread 中的ID是两个概念;
  • Epoch:偏向锁撤销的计数器,可用于偏向锁批量重偏向和批量撤销的判断依据;

    • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针;
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器 Monitor 的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到 Monitor 以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向 Monitor 的指针;

  • 锁升级流程(偏向锁 → 轻量级锁 → 重量级锁)

    • 偏向锁(Biased Locking)

      • 适用场景:单线程重复加锁,无竞争;
      • 加锁:JVM 检测到锁未被偏向 → 用 CAS 将 Mark Word 的“线程 ID”标记为当前线程,锁标志位为 01(偏向模式);
      • 解锁:无需操作(因为无竞争,直接复用);
    • 轻量级锁(Lightweight Locking)

      • 触发条件:出现线程竞争(比如另一个线程尝试加锁);

      • 加锁:竞争线程在栈中创建“锁记录(Lock Record)”,用 CAS 将 Mark Word 指向自己的锁记录,锁标志位改为 00

      • 解锁:用 CAS 将 Mark Word 恢复为无锁状态;若失败,说明有竞争,升级为重量级锁;

    • 重量级锁(Heavyweight Locking)

      • 触发条件:轻量级锁 CAS 失败(竞争激烈),或线程调用 wait()/notify()

      • 加锁:Mark Word 指向 ObjectMonitor(监视器对象),锁标志位改为 10;竞争线程进入 ObjectMonitor 的阻塞队列;

      • 解锁:唤醒阻塞队列中的线程,重新竞争锁;、
        在这里插入图片描述

  • 锁升级的决策逻辑

    • 偏向锁 → 轻量级锁:当有其他线程竞争锁时,JVM 尝试用 CAS 升级为轻量级锁;若 CAS 失败(竞争激烈),直接升级为重量级锁;

    • 轻量级锁 → 重量级锁:轻量级锁解锁时,若发现 Mark Word 已被修改(有其他线程竞争),则升级为重量级锁,进入阻塞队列。

2.6.1.5 代码演示锁升级的过程
import org.openjdk.jol.info.ClassLayout;public class LockUpgrade {public static void main(String[] args) throws InterruptedException {// 创建User对象,初始状态为无锁User userTemp = new User();// 打印无锁状态的对象布局:Mark Word最后3位为001(无锁标志)// 此时对象头存储哈希码、GC分代年龄等信息,无线程相关标记System.out.println("无锁状态(001):"+ ClassLayout.parseInstance(userTemp).toPrintable());/* * JVM默认延迟4秒开启偏向锁机制(避免启动时的竞争影响偏向锁效率)* 可通过-XX:BiasedLockingStartupDelay=0参数取消延迟* 若要禁用偏向锁,使用-XX:-UseBiasedLocking=false*/Thread.sleep(5000);  // 等待偏向锁机制启用// 新创建对象,此时偏向锁机制已启用,对象初始状态为"可偏向"User user = new User();// 打印可偏向状态的对象布局:Mark Word最后3位为101(偏向锁标志,未关联线程)// 此时对象头存储偏向锁相关标记(如epoch),但未记录线程IDSystem.out.println("启用偏向锁(101):"+ ClassLayout.parseInstance(user).toPrintable());// 主线程两次获取并释放偏向锁,验证偏向锁特性for(int i=0;i<2;i++){// 第一次进入同步块:主线程获取偏向锁// JVM通过CAS将对象头的线程ID标记为主线程IDsynchronized (user){// 打印偏向锁状态:Mark Word最后3位101,且记录了当前线程ID// 偏向锁重入时无需CAS,直接复用锁System.out.println("偏向锁(101)(带线程id):"+ ClassLayout.parseInstance(user).toPrintable());}// 偏向锁释放后不会清除线程ID标记(这是偏向锁的特性)// 下次同一线程获取时无需重新CAS,直接验证线程ID即可System.out.println("偏向锁释放(101)(带线程id):"+ ClassLayout.parseInstance(user).toPrintable());}// 创建线程1,触发偏向锁撤销与轻量级锁升级new Thread(new Runnable() {@Overridepublic void run() {// 线程1尝试获取已偏向主线程的锁,触发偏向锁撤销// 撤销后升级为轻量级锁:通过CAS将Mark Word指向线程1的锁记录synchronized (user){// 打印轻量级锁状态:Mark Word最后2位为00(轻量级锁标志)// 此时对象头存储指向线程栈中锁记录的指针System.out.println("轻量级锁(00):"+ ClassLayout.parseInstance(user).toPrintable());try {System.out.println("睡眠3秒========================");Thread.sleep(3000);  // 保持锁3秒,让线程2有机会竞争} catch (InterruptedException e) {throw new RuntimeException(e);}// 线程2此时已尝试竞争锁,轻量级锁CAS失败,升级为重量级锁// 重量级锁标志为10,对象头存储指向ObjectMonitor的指针System.out.println("轻量级锁--->重量级锁(10):"+ ClassLayout.parseInstance(user).toPrintable());}}}).start();// 等待1秒,确保线程1已获取轻量级锁并进入睡眠Thread.sleep(1000);// 此时线程1持有锁,主线程观察到对象已升级为重量级锁System.out.println("重量级锁(10):" + ClassLayout.parseInstance(user).toPrintable());// 创建线程2,加剧锁竞争,巩固重量级锁状态new Thread(new Runnable() {@Overridepublic void run() {// 线程2竞争重量级锁,进入ObjectMonitor的阻塞队列synchronized (user) {// 成功获取重量级锁后,对象头仍保持10标志System.out.println("重量级锁(10):" + ClassLayout.parseInstance(user).toPrintable());}}}).start();// 等待5秒,确保所有线程执行完毕,锁被释放Thread.sleep(5000);// 重量级锁释放后不会自动降级为轻量级锁或偏向锁(锁升级是单向的)// 此处打印的"无锁状态"仅为演示,实际重量级锁释放后仍可能保留monitor指针System.out.println("无锁状态(001):" + ClassLayout.parseInstance(user).toPrintable());}// 定义简单的User类作为锁对象static class User {}
}
  • 代码核心逻辑:
    • 无锁状态:新创建的对象默认处于无锁状态,Mark Word 最后 3 位为001,存储对象哈希码、GC 分代年龄等信息,无任何线程标记;
    • 偏向锁启用:等待 5 秒后(JVM 默认的偏向锁延迟),新创建的对象进入"可偏向" 状态(标志位101),此时对象头已准备好记录偏向的线程 ID,但尚未关联任何线程;
    • 偏向锁获取与释放
      • 主线程第一次获取锁时,JVM 通过 CAS 将对象头的线程 ID 标记为主线程 ID,标志位保持101
      • 偏向锁释放时不会清除线程 ID,下次同一线程获取时无需重新 CAS,直接复用(体现偏向锁 “偏向” 特性);
    • 偏向锁→轻量级锁升级:线程 1 尝试获取已偏向主线程的锁,触发偏向锁撤销(因为出现竞争),锁升级为轻量级锁(标志位00),此时对象头存储指向线程 1 栈中锁记录的指针;
    • 轻量级锁→重量级锁升级:线程 1 持有轻量级锁时,线程 2 尝试竞争,导致轻量级锁 CAS 操作失败。由于竞争激烈,锁升级为重量级锁(标志位10),对象头转而存储指向ObjectMonitor的指针,线程进入阻塞队列等待;
    • 锁升级的单向性:重量级锁释放后不会自动降级为轻量级锁或偏向锁,这是 JVM 设计的简化策略(降级成本高于收益)。
2.6.1.4 思考题
  • 问:重量级锁释放之后变为无锁,此时有新的线程来调用同步块,会获取什么锁?答:轻量锁

  • 按照无锁 -> 偏向锁 -> 轻量锁 -> 重量锁的流程,那不应该加的是偏向锁吗?为什么是轻量锁?

    • 原因:因为偏向锁在大多数现代应用中带来的收益已经不如其维护成本,自JDK 15起,偏向锁被默认禁用了
  • 偏向锁的设计初衷与问题

    • 初衷:在“锁大多情况下不仅不存在竞争,而且总是由同一线程持有”的场景下,为了避免同一线程重复执行CAS操作(轻量级锁的加锁解锁流程),引入了偏向锁。一旦线程获得了偏向锁,后续再进入同步块只需要简单检查一下线程ID即可,开销极小;
    • 问题
      1. 维护成本高:偏向锁的撤销(Revoke)过程需要进入安全点(SafePoint),这意味着需要暂停所有用户线程(STW),这个操作是非常昂贵的;
      2. 收益下降:随着并发编程的发展和开发者对线程安全意识的提高,很多JDK自身的核心类库(如StringBuffer, HashTable等)已经不再作为共享资源被频繁使用,真正的“无竞争”场景并没有想象中那么多。在很多高性能应用中,锁要么是无竞争的轻量级锁,要么是竞争激烈的重量级锁,那种“一开始无竞争后来有竞争”的场景,反而触发了昂贵的偏向锁撤销流程,得不偿失;
  • 由于上述原因,Oracle公司决定逐步弃用并最终默认禁用偏向锁。

    • JDK 15 开始,偏向锁被默认禁用。同时,相关的命令行选项(如-XX:+UseBiasedLocking)被标记为“废弃”。

    • 即使是在JDK 15之前,很多基于大量线程的应用程序(如Web服务器、微服务)也会通过显式添加JVM参数 -XX:-UseBiasedLocking主动关闭偏向锁,以消除撤销偏向锁带来的STW开销,获得更稳定的性能表现。

  • 由于偏向锁被默认禁用,现在(以JDK 17/21等LTS版本为例)的加锁流程实际上是:无锁 -> 轻量级锁 -> (如果需要) -> 重量级锁。所以,在思考题的场景中:

    1. 锁被释放后,状态变为“无锁”;
    2. 新的线程来加锁时,JVM看到偏向锁是禁用的,因此直接跳过偏向锁的流程
    3. 它尝试使用CAS操作来获取轻量级锁(在自己的栈帧中创建锁记录,尝试替换Mark Word等);
    4. 如果CAS成功(无竞争),则持有轻量级锁;
    5. 如果CAS失败(有竞争),则立即膨胀为重量级锁

2.6.2 轻量级锁详解

  • 轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁;

  • 思考: 轻量级锁是否可以降级为偏向锁?答:不能。原因:锁升级是单向不可逆 的:

    • 偏向锁 → 轻量级锁 → 重量级锁(一旦升级,无法自动降级);

    • 轻量级锁的设计目标是“处理低竞争但比偏向锁稍激烈的场景”,偏向锁则针对“单线程无竞争”场景;

    • 若允许降级,需要额外的状态回退逻辑,复杂度高且收益低(JVM 选择简化设计);

  • 在介绍轻量级锁的原理之前,再看看之前 MarkWord 图:轻量级锁操作的就是对象头的 MarkWord

    在这里插入图片描述

  • 加锁过程:

    • 如果判断出当前处于无锁状态,线程会在栈中创建一个名叫 **LockRecord(锁记录)**的区域,然后把锁对象的 MarkWord 拷贝一份到 LockRecord 中,该副本称之为 dhw

    • 然后用 CAS 操作,尝试将锁对象的 Mark Word 指向当前线程的 LockRecord:

      • 成功:加锁完成(轻量级锁状态,Mark Word 标志位为 00);
      • 失败:说明有竞争,直接升级为重量级锁(标志位为 10);

    在这里插入图片描述

  • 解锁流程

    • 用 CAS 操作,将 LockRecord 中存储的 dhw 写回锁对象的 Mark Word;

    • 判断 CAS 是否成功:

      • 成功:解锁完成(无竞争);
      • 失败:说明解锁时已有其他线程竞争 → 触发锁膨胀(升级为重量级锁);

    在这里插入图片描述

    • 如果当前线程已经持有轻量级锁(重入):
      • 加锁时,将 LockRecord 的 dhw 设为 null(标记为重入);

      • 解锁时,若 dhwnull,直接回退(无需 CAS),避免重复操作;

    在这里插入图片描述

    在这里插入图片描述

2.6.3 偏向锁详解

  • 偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。
2.6.3.1 偏向锁匿名偏向状态
  • 当 JVM 启用了偏向锁模式(Jdk6 默认开启),新创建的对象在 4 秒后、未被任何线程获取时,处于匿名偏向状态(Anonymously Biased):
    • Mark Word 中的 Thread ID0偏向锁标志位为 1(锁标志位 01);
    • 表示可偏向,但还没偏向任何线程,一旦有线程获取锁,会将 Thread ID 标记为当前线程。
2.6.3.2 偏向锁的“延迟启用”机制
  • JVM 启动时(比如加载类、初始化系统类),会大量使用 synchronized,这些锁通常不会被竞争(单线程执行)。如果直接启用偏向锁,JVM 需要为这些对象做偏向锁标记,反而增加初始化时间。因此,HotSpot 设计了**“4 秒延迟启用偏向锁”**的机制:

    • 启动后前 4 秒,新创建的对象不启用偏向锁(处于无锁或轻量级锁状态);

    • 4 秒后,新创建的对象才会开启偏向锁模式(可偏向但未偏向线程,即“匿名偏向状态”);

  • 控制延迟的参数

    • 关闭延迟-XX:BiasedLockingStartupDelay=0(让偏向锁立即启用);

    • 禁用偏向锁-XX:-UseBiasedLocking(完全关闭偏向锁优化);

    • 启用偏向锁-XX:+UseBiasedLocking(默认开启);

  • 代码实验:跟踪偏向锁状态变化

    public class LockEscalationDemo {public static void main(String[] args) throws InterruptedException {log.debug(ClassLayout.parseInstance(new Object()).toPrintable());// HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式Thread.sleep(4000);// 创建 obj 对象Object obj = new Object();// 创建并启动thread1来竞争锁new Thread(new Runnable() {@Overridepublic void run() {log.debug(Thread.currentThread().getName()+"开始执行。。。\n"+ClassLayout.parseInstance(obj).toPrintable());synchronized (obj){log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"+ClassLayout.parseInstance(obj).toPrintable());}log.debug(Thread.currentThread().getName()+"释放锁。。。\n"+ClassLayout.parseInstance(obj).toPrintable());}},"thread1").start();Thread.sleep(5000);log.debug(ClassLayout.parseInstance(obj).toPrintable());}
    }
    
  • 状态变化流程:

    1. 线程启动前(匿名偏向状态)obj 的 Mark Word 中 Thread ID=0,标志位 01(匿名偏向);

    2. 线程获取锁(偏向锁生效)thread1 执行 synchronized (obj) → JVM 用 CAS 将 Thread ID 标记为 thread1 → 偏向锁生效(标志位仍为 01,但 Thread ID 非 0);

    3. 线程释放锁(偏向锁保留):释放锁后,obj 的 Mark Word 仍保留 thread1Thread ID(偏向锁不会自动清除,下次同一线程可直接复用);

  • 结果解读:

    • thread1 开始执行:obj 的 Mark Word 中 Thread ID=0 → 匿名偏向状态(4 秒后创建的对象,未被竞争);

    • “thread1 获取锁执行中”:Thread ID 变为 thread1 的 ID → 偏向锁生效(标志位 01);

    • “thread1 释放锁”:Thread ID 仍为 thread1 的 ID → 偏向锁保留(下次同一线程可直接获取,无需 CAS);

    在这里插入图片描述

2.6.3.3 思考题
  • 问:如果对象调用了 hashCode,还会开启偏向锁模式吗?

  • 答:调用 Object.hashCode() 或重写的 hashCode() 后,对象的 Mark Word 会存储哈希码,这会破坏偏向锁的状态,导致偏向锁无法启用;

  • 偏向锁与哈希码的冲突:

    • 偏向锁的 Mark Word 存储:

      • 偏向线程 ID、Epoch、分代年龄、锁标志位(01);

      • 没有预留哈希码的空间(为了节省内存,复用 Mark Word 区域);

    • 当对象调用 hashCode() 时:

      • JVM 会强制计算哈希码,并将其存储到 Mark Word(覆盖原本可能存储的偏向锁信息);

      • 这会导致偏向锁的“线程 ID、Epoch”等信息丢失 → 偏向锁无法启用(对象不再处于可偏向状态)。

2.6.3.4 偏向锁撤销场景之调用对象HashCode
  • 当锁对象调用 hashCode()System.identityHashCode(obj) 时,会导致该对象的偏向锁被撤销;

  • 冲突本质:偏向锁的 Mark Word 没有预留存储哈希码的空间(为了节省内存,复用标记位)

    • 当锁对象调用 hashCode()System.identityHashCode(obj) 时,JVM 必须在 Mark Word 中存储哈希码 → 覆盖偏向锁的线程 ID、Epoch 等信息;

    • 这会导致偏向锁无法维持,触发锁撤销(升级为轻量级锁或重量级锁);

  • 当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用 HashCode 计算将会使对象再也无法偏向:

    • 对象处于“可偏向”状态(线程 ID=0 ):调用 hashCode() → Mark Word 存储哈希码 → 锁状态变为无锁(但存储了哈希码),后续无法再进入偏向锁;
  • 对象处于“已偏向”状态(线程 ID≠0 ):调用 hashCode() → 触发偏向锁撤销 → 升级为轻量级锁(在栈中创建 LockRecord,存储哈希码)。

2.6.3.5 偏向锁撤销场景之调用wait/notify
  • 偏向锁状态执行obj.notify()会升级为轻量级锁,调用obj.wait(timeout)会升级为重量级锁

    • waitnotifyObject 的 native 方法,依赖 ObjectMonitor(重量级锁的实现);

    • 偏向锁的 Mark Word 不包含 ObjectMonitor 的指针 → 调用 wait/notify 时,必须升级为重量级锁(才能关联 ObjectMonitor);

  • 具体行为

    • 调用 notify():偏向锁状态下调用 obj.notify() → 触发偏向锁撤销 → 升级为轻量级锁(因为 notify 不需要阻塞,但需要关联 ObjectMonitor 的等待队列);
  • 调用 wait(timeout):偏向锁状态下调用 obj.wait(100) → 触发偏向锁撤销 → 直接升级为重量级锁(因为 wait 会让线程进入阻塞队列,必须依赖 ObjectMonitor)。

  • 偏向锁的设计目标是 “单线程无竞争场景”,一旦遇到以下操作,会触发撤销(升级为更重的锁):

    • 调用 hashCode()(需要存储哈希码,破坏偏向锁布局);
    • 调用 wait/notify(需要关联 ObjectMonitor,依赖重量级锁);
    • 出现多线程竞争(其他线程尝试获取偏向锁)。
2.6.3.6 偏向锁批量重偏向&批量撤销
  • 偏向锁的痛点:当多线程竞争偏向锁时,每次撤销偏向锁需要等到 safe point(安全点,JVM 暂停线程的时机),并升级为轻量级锁或重量级锁 → 频繁撤销会消耗性能,甚至让偏向锁从“优化”变成“负优化”,于是就有了了批量重偏向与批量撤销的机制;

  • 实现原理

    • 以**Class为单位**,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1。统计偏向锁撤销次数,当次数达到批量重偏向阈值(默认 20)时:JVM就认为该class的偏向锁有问题,因此会进行批量重偏向
    • 当达到批量重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销阈值(默认 40)后,JVM 就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑;
  • 关键参数

    • BiasedLockingBulkRebiasThreshold:批量重偏向阈值(默认 20);

    • BiasedLockingBulkRevokeThreshold:批量撤销阈值(默认 40);

    • 可通过 JVM 参数调整:-XX:BiasedLockingBulkRebiasThreshold-XX:BiasedLockingBulkRevokeThreshold

  • 应用场景

    • 批量重偏向(Bulk Rebias)

      • 适用场景:一个线程创建大量对象并执行同步操作(对象偏向该线程),后续另一个线程也竞争这些对象的锁 → 大量偏向锁需要撤销;

      • 作用:通过“批量重偏向”,让新竞争的线程成为对象的偏向线程 → 避免逐个对象撤销偏向锁,减少性能开销;

    • 批量撤销(Bulk Revoke)

      • 适用场景:多线程竞争激烈,即使重偏向也无法缓解 → 偏向锁频繁撤销,性能下降;

      • 作用:标记 Class 为“不可偏向”,后续对象直接走轻量级锁 → 避免偏向锁的额外开销;

  • 补充:

    • 批量重偏向和批量撤销是针对类的优化,和对象无关

      • 批量重偏向、批量撤销的触发与决策,是以 Class(类) 为单位进行的,而非单个对象;
        • JVM 会为每个类维护偏向锁撤销计数器,统计该类所有对象的撤销行为;
        • 当计数器达到阈值(重偏向或批量撤销阈值),JVM 会对整个类的后续对象调整锁策略,而不是针对某一个对象单独处理;
    • 例:假设 Order.class 有 100 个对象,其中 20 个对象因线程竞争触发偏向锁撤销 → JVM 统计到 Order 类的撤销次数达阈值,会对所有新创建的 Order 对象执行批量重偏向或撤销,影响的是整个类的行为;

  • 偏向锁重偏向一次之后不可再次重偏向

    • 批量重偏向是一次性的调整:当某个类触发批量重偏向(撤销次数达重偏向阈值),JVM 会让该类新创建的对象偏向当前竞争的线程,但后续不会再次触发“批量重偏向”

      • 设计意图:避免无限重偏向导致锁策略“反复横跳”,增加复杂度。若后续仍有新线程竞争,直接进入“批量撤销”流程(若达到撤销阈值),让锁策略向“轻量级锁”过渡,简化逻辑;

      • 流程示例

        • User 类触发批量重偏向 → 新 User 对象偏向线程 B;
      • 若线程 C 又竞争 User 对象 → 不会再次重偏向,而是统计撤销次数,达阈值则触发批量撤销;

    • 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利

      • 触发后果:批量撤销是 JVM 应对“持续激烈竞争”的终极手段:一旦某个类触发批量撤销(撤销次数达批量撤销阈值),JVM 判定该类的使用场景不适合偏向锁(多线程竞争频繁);

      • 后续行为:该类新创建的对象会直接跳过“偏向锁”阶段,默认使用轻量级锁(或更高阶的锁策略),避免因偏向锁的频繁撤销带来额外开销;

      • 实际影响:比如 Product 类触发批量撤销后,所有新 Product 对象不再尝试偏向锁,同步块直接以轻量级锁逻辑执行,牺牲“偏向锁的轻量优势”换取“竞争场景下的稳定性”。

附录:JVM指令集

指令码 助记符    说明
0x00 nop      什么都不做
0x01 aconst_null 将null推送至栈顶
0x02 iconst_m1   将int型-1推送至栈顶
0x03 iconst_0   将int型0推送至栈顶
0x04 iconst_1   将int型1推送至栈顶
0x05 iconst_2   将int型2推送至栈顶
0x06 iconst_3   将int型3推送至栈顶
0x07 iconst_4   将int型4推送至栈顶
0x08 iconst_5   将int型5推送至栈顶
0x09 lconst_0   将long型0推送至栈顶
0x0a lconst_1   将long型1推送至栈顶
0x0b fconst_0   将float型0推送至栈顶
0x0c fconst_1   将float型1推送至栈顶
0x0d fconst_2   将float型2推送至栈顶
0x0e dconst_0   将double型0推送至栈顶
0x0f dconst_1   将double型1推送至栈顶
0x10 bipush    将单字节的常量值(-128~127)推送至栈顶
0x11 sipush    将一个短整型常量值(-32768~32767)推送至栈顶
0x12 ldc      将int, float或String型常量值从常量池中推送至栈顶
0x13 ldc_w     将int, float或String型常量值从常量池中推送至栈顶(宽索引)
0x14 ldc2_w    将long或double型常量值从常量池中推送至栈顶(宽索引)
0x15 iload     将指定的int型本地变量推送至栈顶
0x16 lload     将指定的long型本地变量推送至栈顶
0x17 fload     将指定的float型本地变量推送至栈顶
0x18 dload     将指定的double型本地变量推送至栈顶
0x19 aload     将指定的引用类型本地变量推送至栈顶
0x1a iload_0    将第一个int型本地变量推送至栈顶
0x1b iload_1    将第二个int型本地变量推送至栈顶
0x1c iload_2    将第三个int型本地变量推送至栈顶
0x1d iload_3    将第四个int型本地变量推送至栈顶
0x1e lload_0    将第一个long型本地变量推送至栈顶
0x1f lload_1    将第二个long型本地变量推送至栈顶
0x20 lload_2    将第三个long型本地变量推送至栈顶
0x21 lload_3    将第四个long型本地变量推送至栈顶
0x22 fload_0    将第一个float型本地变量推送至栈顶
0x23 fload_1    将第二个float型本地变量推送至栈顶
0x24 fload_2    将第三个float型本地变量推送至栈顶
0x25 fload_3    将第四个float型本地变量推送至栈顶
0x26 dload_0    将第一个double型本地变量推送至栈顶
0x27 dload_1    将第二个double型本地变量推送至栈顶
0x28 dload_2    将第三个double型本地变量推送至栈顶
0x29 dload_3    将第四个double型本地变量推送至栈顶
0x2a aload_0    将第一个引用类型本地变量推送至栈顶
0x2b aload_1    将第二个引用类型本地变量推送至栈顶
0x2c aload_2    将第三个引用类型本地变量推送至栈顶
0x2d aload_3    将第四个引用类型本地变量推送至栈顶
0x2e iaload    将int型数组指定索引的值推送至栈顶
0x2f laload    将long型数组指定索引的值推送至栈顶
0x30 faload    将float型数组指定索引的值推送至栈顶
0x31 daload    将double型数组指定索引的值推送至栈顶
0x32 aaload    将引用型数组指定索引的值推送至栈顶
0x33 baload    将boolean或byte型数组指定索引的值推送至栈顶
0x34 caload    将char型数组指定索引的值推送至栈顶
0x35 saload    将short型数组指定索引的值推送至栈顶
0x36 istore    将栈顶int型数值存入指定本地变量
0x37 lstore    将栈顶long型数值存入指定本地变量
0x38 fstore    将栈顶float型数值存入指定本地变量
0x39 dstore    将栈顶double型数值存入指定本地变量
0x3a astore    将栈顶引用型数值存入指定本地变量
0x3b istore_0   将栈顶int型数值存入第一个本地变量
0x3c istore_1   将栈顶int型数值存入第二个本地变量
0x3d istore_2   将栈顶int型数值存入第三个本地变量
0x3e istore_3   将栈顶int型数值存入第四个本地变量
0x3f lstore_0   将栈顶long型数值存入第一个本地变量
0x40 lstore_1   将栈顶long型数值存入第二个本地变量
0x41 lstore_2   将栈顶long型数值存入第三个本地变量
0x42 lstore_3   将栈顶long型数值存入第四个本地变量
0x43 fstore_0   将栈顶float型数值存入第一个本地变量
0x44 fstore_1   将栈顶float型数值存入第二个本地变量
0x45 fstore_2   将栈顶float型数值存入第三个本地变量
0x46 fstore_3   将栈顶float型数值存入第四个本地变量
0x47 dstore_0   将栈顶double型数值存入第一个本地变量
0x48 dstore_1   将栈顶double型数值存入第二个本地变量
0x49 dstore_2   将栈顶double型数值存入第三个本地变量
0x4a dstore_3   将栈顶double型数值存入第四个本地变量
0x4b astore_0   将栈顶引用型数值存入第一个本地变量
0x4c astore_1   将栈顶引用型数值存入第二个本地变量
0x4d astore_2   将栈顶引用型数值存入第三个本地变量
0x4e astore_3   将栈顶引用型数值存入第四个本地变量
0x4f iastore    将栈顶int型数值存入指定数组的指定索引位置
0x50 lastore    将栈顶long型数值存入指定数组的指定索引位置
0x51 fastore    将栈顶float型数值存入指定数组的指定索引位置
0x52 dastore    将栈顶double型数值存入指定数组的指定索引位置
0x53 aastore    将栈顶引用型数值存入指定数组的指定索引位置
0x54 bastore    将栈顶boolean或byte型数值存入指定数组的指定索引位置
0x55 castore    将栈顶char型数值存入指定数组的指定索引位置
0x56 sastore    将栈顶short型数值存入指定数组的指定索引位置
0x57 pop      将栈顶数值弹出 (数值不能是long或double类型的)
0x58 pop2     将栈顶的一个(long或double类型的)或两个数值弹出(其它)
0x59 dup      复制栈顶数值并将复制值压入栈顶
0x5a dup_x1    复制栈顶数值并将两个复制值压入栈顶
0x5b dup_x2    复制栈顶数值并将三个(或两个)复制值压入栈顶
0x5c dup2     复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶
0x5d dup2_x1    <待补充>
0x5e dup2_x2    <待补充>
0x5f swap     将栈最顶端的两个数值互换(数值不能是long或double类型的)
0x60 iadd     将栈顶两int型数值相加并将结果压入栈顶
0x61 ladd     将栈顶两long型数值相加并将结果压入栈顶
0x62 fadd     将栈顶两float型数值相加并将结果压入栈顶
0x63 dadd     将栈顶两double型数值相加并将结果压入栈顶
0x64 isub     将栈顶两int型数值相减并将结果压入栈顶
0x65 lsub     将栈顶两long型数值相减并将结果压入栈顶
0x66 fsub     将栈顶两float型数值相减并将结果压入栈顶
0x67 dsub     将栈顶两double型数值相减并将结果压入栈顶
0x68 imul     将栈顶两int型数值相乘并将结果压入栈顶
0x69 lmul     将栈顶两long型数值相乘并将结果压入栈顶
0x6a fmul     将栈顶两float型数值相乘并将结果压入栈顶
0x6b dmul     将栈顶两double型数值相乘并将结果压入栈顶
0x6c idiv     将栈顶两int型数值相除并将结果压入栈顶
0x6d ldiv     将栈顶两long型数值相除并将结果压入栈顶
0x6e fdiv     将栈顶两float型数值相除并将结果压入栈顶
0x6f ddiv     将栈顶两double型数值相除并将结果压入栈顶
0x70 irem     将栈顶两int型数值作取模运算并将结果压入栈顶
0x71 lrem     将栈顶两long型数值作取模运算并将结果压入栈顶
0x72 frem     将栈顶两float型数值作取模运算并将结果压入栈顶
0x73 drem     将栈顶两double型数值作取模运算并将结果压入栈顶
0x74 ineg     将栈顶int型数值取负并将结果压入栈顶
0x75 lneg     将栈顶long型数值取负并将结果压入栈顶
0x76 fneg     将栈顶float型数值取负并将结果压入栈顶
0x77 dneg     将栈顶double型数值取负并将结果压入栈顶
0x78 ishl     将int型数值左移位指定位数并将结果压入栈顶
0x79 lshl     将long型数值左移位指定位数并将结果压入栈顶
0x7a ishr     将int型数值右(符号)移位指定位数并将结果压入栈顶
0x7b lshr     将long型数值右(符号)移位指定位数并将结果压入栈顶
0x7c iushr     将int型数值右(无符号)移位指定位数并将结果压入栈顶
0x7d lushr     将long型数值右(无符号)移位指定位数并将结果压入栈顶
0x7e iand     将栈顶两int型数值作“按位与”并将结果压入栈顶
0x7f land     将栈顶两long型数值作“按位与”并将结果压入栈顶
0x80 ior      将栈顶两int型数值作“按位或”并将结果压入栈顶
0x81 lor      将栈顶两long型数值作“按位或”并将结果压入栈顶
0x82 ixor     将栈顶两int型数值作“按位异或”并将结果压入栈顶
0x83 lxor     将栈顶两long型数值作“按位异或”并将结果压入栈顶
0x84 iinc     将指定int型变量增加指定值(i++, i--, i+=2)
0x85 i2l      将栈顶int型数值强制转换成long型数值并将结果压入栈顶
0x86 i2f      将栈顶int型数值强制转换成float型数值并将结果压入栈顶
0x87 i2d      将栈顶int型数值强制转换成double型数值并将结果压入栈顶
0x88 l2i      将栈顶long型数值强制转换成int型数值并将结果压入栈顶
0x89 l2f      将栈顶long型数值强制转换成float型数值并将结果压入栈顶
0x8a l2d      将栈顶long型数值强制转换成double型数值并将结果压入栈顶
0x8b f2i      将栈顶float型数值强制转换成int型数值并将结果压入栈顶
0x8c f2l      将栈顶float型数值强制转换成long型数值并将结果压入栈顶
0x8d f2d      将栈顶float型数值强制转换成double型数值并将结果压入栈顶
0x8e d2i      将栈顶double型数值强制转换成int型数值并将结果压入栈顶
0x8f d2l      将栈顶double型数值强制转换成long型数值并将结果压入栈顶
0x90 d2f      将栈顶double型数值强制转换成float型数值并将结果压入栈顶
0x91 i2b      将栈顶int型数值强制转换成byte型数值并将结果压入栈顶
0x92 i2c      将栈顶int型数值强制转换成char型数值并将结果压入栈顶
0x93 i2s      将栈顶int型数值强制转换成short型数值并将结果压入栈顶
0x94 lcmp     比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶
0x95 fcmpl     比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x96 fcmpg     比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
0x97 dcmpl     比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x98 dcmpg     比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
0x99 ifeq     当栈顶int型数值等于0时跳转
0x9a ifne     当栈顶int型数值不等于0时跳转
0x9b iflt     当栈顶int型数值小于0时跳转
0x9c ifge     当栈顶int型数值大于等于0时跳转
0x9d ifgt     当栈顶int型数值大于0时跳转
0x9e ifle     当栈顶int型数值小于等于0时跳转
0x9f if_icmpeq   比较栈顶两int型数值大小,当结果等于0时跳转
0xa0 if_icmpne   比较栈顶两int型数值大小,当结果不等于0时跳转
0xa1 if_icmplt   比较栈顶两int型数值大小,当结果小于0时跳转
0xa2 if_icmpge   比较栈顶两int型数值大小,当结果大于等于0时跳转
0xa3 if_icmpgt   比较栈顶两int型数值大小,当结果大于0时跳转
0xa4 if_icmple   比较栈顶两int型数值大小,当结果小于等于0时跳转
0xa5 if_acmpeq   比较栈顶两引用型数值,当结果相等时跳转
0xa6 if_acmpne   比较栈顶两引用型数值,当结果不相等时跳转
0xa7 goto     无条件跳转
0xa8 jsr      跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶
0xa9 ret      返回至本地变量指定的index的指令位置(一般与jsr, jsr_w联合使用)
0xaa tableswitch    用于switch条件跳转,case值连续(可变长度指令)
0xab lookupswitch   用于switch条件跳转,case值不连续(可变长度指令)
0xac ireturn    从当前方法返回int
0xad lreturn    从当前方法返回long
0xae freturn    从当前方法返回float
0xaf dreturn    从当前方法返回double
0xb0 areturn    从当前方法返回对象引用
0xb1 return    从当前方法返回void
0xb2 getstatic   获取指定类的静态域,并将其值压入栈顶
0xb3 putstatic   为指定的类的静态域赋值
0xb4 getfield   获取指定类的实例域,并将其值压入栈顶
0xb5 putfield   为指定的类的实例域赋值
0xb6 invokevirtual   调用实例方法
0xb7 invokespecial   调用超类构造方法,实例初始化方法,私有方法
0xb8 invokestatic   调用静态方法
0xb9 invokeinterface 调用接口方法
0xba --
0xbb new      创建一个对象,并将其引用值压入栈顶
0xbc newarray   创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶
0xbd anewarray   创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶
0xbe arraylength 获得数组的长度值并压入栈顶
0xbf athrow    将栈顶的异常抛出
0xc0 checkcast   检验类型转换,检验未通过将抛出ClassCastException
0xc1 instanceof 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
0xc2 monitorenter   获得对象的锁,用于同步方法或同步块
0xc3 monitorexit    释放对象的锁,用于同步方法或同步块
0xc4 wide     <待补充>
0xc5 multianewarray 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶
0xc6 ifnull    为null时跳转
0xc7 ifnonnull   不为null时跳转
0xc8 goto_w    无条件跳转(宽索引)
0xc9 jsr_w     跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶
http://www.dtcms.com/a/355687.html

相关文章:

  • 优雅地实现ChatGPT式的打字机效果:Spring Boot 流式响应
  • Jtekt深沟球轴承外圈防跑圈开发
  • Python Imaging Library (PIL) 全面指南:PIL基础入门-图像颜色模式转换与应用
  • [网鼎杯 2018]Fakebook
  • 基础IO详解
  • 【前端教程】JavaScript 基础总结
  • 教育类《河北教育》杂志简介
  • Day03_苍穹外卖——公共字段自动填充菜品相关功能
  • 河南萌新联赛2025第(七)场:郑州轻工业大学
  • 【数据结构与算法】(LeetCode)141.环形链表 142.环形链表Ⅱ
  • 数据分析学习笔记4:加州房价预测
  • 国产的服务器
  • 如何监控PCIe 5.0 SSD的运行温度?多软件推荐
  • 中国剩余定理(以及扩展..)
  • 用 Docker 玩转 Kafka 4.0镜像选型、快速起步、配置持久化与常见坑
  • 影楼精修-锁骨增强算法
  • 深入理解 PHP 中的 `pcntl_fork()`:父进程与子进程的执行路径
  • SRE网易一面面经
  • Linux笔记12——shell编程基础-6
  • 少样本图异常检测系列【A Survey of Few-Shot Graph Anomaly Detection】
  • Python实战:银行ATM系统开发全解析
  • RuoYi-VuePlus:前端指定接口不显示错误提示
  • 面试tips--JVM(2)--对象创建的过程
  • ERNIE-4.5-VL:技术解密+应用实战,解锁多模态新场景!
  • 8.29 贪心|摩尔投票
  • 【不说废话】pytorch中.to(device)函数详解
  • 基于K8s部署服务:dev、uat、prod环境的核心差异解析
  • 工业级TF卡NAND+北京君正+Rk瑞芯微的应用
  • openEuler Embedded 的 Yocto入门 : 5.基本变量与基本任务详解
  • Linux 系统 poll 与 epoll 机制1:实现原理与应用实践