3. 第三个概念集合——对synchronized锁,原理,概念,API调用的集合,以及锁升级,在使用API的时候,发生了什么事情

synchronized的使用背景和基础概念

这个在JavaSE的时候就有介绍。
就是因为线程多线程,在Java中的时候,数据不安全。
线程访问数据,到线程的内存里
不是CPU的级别的内存
会导致超卖的问题。

这个时候,官方已经解决这个问题,就是创造了这个synchronized关键字。

这个时候使用synchronized这个锁,就可以保证,
同一时间,只会有一个线程访问内存里的数据。

我们基础层面的掌握,就是掌握一下,什么时候使用这个关键字,怎么使用这个关键字。

补充说明:
这里的“线程的内存”指的是Java内存模型(JMM)中定义的工作内存(线程私有栈内存中的变量副本),而不是CPU缓存。每个线程从主内存拷贝共享变量到自己的工作内存中操作,操作完成后再写回主内存,这个过程如果不加控制就会导致可见性问题。
“超卖问题” 典型场景如:两个线程同时读取到库存=1,各自在工作内存中减到0,再先后写回主内存,最终结果还是0,但实际卖出了两份。synchronized通过互斥访问解决了这种并发修改的不安全问题。


synchronized在代码使用层面需要了解的概念和知识

synchronized这个关键字,可以修饰什么,怎么使用

补充说明——synchronized的三种使用方式:

  1. 修饰实例方法

    public synchronized void method() {
        // 同步代码
    }
    

    锁对象:当前实例对象(this)

  2. 修饰静态方法

    public static synchronized void staticMethod() {
        // 同步代码
    }
    

    锁对象:当前类的Class对象(如 MyClass.class)

  3. 修饰代码块

    synchronized(lockObject) {
        // 同步代码
    }
    

    锁对象:括号中指定的任意对象

使用选择建议:

  • 方法内只有少量代码需要同步 → 用同步代码块,减少锁持有时间,提高并发度
  • 整个方法都需要同步 → 可以修饰方法,代码更简洁
  • 控制粒度精细(如一个方法内对不同资源分别加锁) → 用不同的锁对象分开控制

synchronized在代码原理层面需要了解的概念和知识

我们看一下synchronized的例子。

// 同步代码块的常见用法
private final Object lock = new Object();  // 专门的锁对象

public void someMethod() {
    synchronized(lock) {
        // 需要同步的代码
    }
}

这个要锁一个对象,为什么是锁一个对象?

我们知道,对象在JVM里,可以分为:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 填充数据(Padding)

对象头里面有丰富的信息,包括:

  • Mark Word(标记字段)
  • Klass Pointer(类型指针)
  • 数组长度(如果对象是数组)

Mark Word 中存储了与锁相关的核心信息:

  • 哈希码(hashCode)
  • GC分代年龄(age)
  • 锁状态标志位(01、00、10、11)
  • 指向线程ID的指针(偏向锁模式下)
  • 指向重量级锁(monitor)的指针

补充说明:为什么锁的是一个对象?

在JVM设计中,每个Java对象都可以天然地作为锁,根本原因就在于对象头的Mark Word中包含了锁状态信息。当线程想要进入同步块时,JVM会检查目标对象的对象头:

  1. 判断当前锁状态(无锁、偏向锁、轻量级锁、重量级锁)
  2. 根据状态决定采用哪种锁机制
  3. 通过CAS或monitor机制竞争锁
  4. 成功则修改对象头的锁状态记录,失败则阻塞或自旋等待

锁对象的选择原则:

  • 多个线程竞争的必须是同一个对象(同一内存地址的同一个实例)
  • 如果是不同对象,就是不同的锁,不会互斥
  • 常用做法:使用private final Object lock = new Object()可以防止锁对象被意外修改

锁升级(Lock Escalation / Biased Locking → Lightweight Lock → Heavyweight Lock)

JVM锁优化的核心机制: 从JDK 6开始,synchronized引入了锁升级机制,避免一上来就使用重量级锁(操作系统的互斥量,需要用户态到内核态切换,开销大),而是根据竞争激烈程度动态调整锁的状态。

锁的状态(级别从低到高):

  1. 无锁 → 2. 偏向锁 → 3. 轻量级锁 → 4. 重量级锁

锁可以升级,但不能降级(偏向锁→轻量级→重量级单向)

(1)偏向锁(Biased Locking)
  • 场景: 锁被同一个线程反复获取,没有竞争
  • 原理: 第一次获取锁时,在对象头的Mark Word中记录该线程ID。后续该线程再进入同步块时,只需检查线程ID是否为当前线程,无需CAS操作
  • 撤销: 当另一个线程来竞争时,偏向锁需要撤销(在全局安全点执行),可能升级为轻量级锁
(2)轻量级锁(Lightweight Locking)
  • 场景: 少量线程交替持有锁,没有实际阻塞
  • 原理: 线程在执行同步块前,在当前线程栈帧中创建锁记录(Lock Record),通过CAS将对象头的Mark Word替换为指向锁记录的指针。成功则获取锁,失败则说明有竞争
  • 特点: 未获取锁的线程会自旋(CAS重试),不自旋失败后才膨胀为重量级锁
(3)重量级锁(Heavyweight Locking)
  • 场景: 多条线程竞争激烈,自旋等待时间过长
  • 原理: 锁膨胀为重量级锁后,对象头的Mark Word指向一个monitor对象(ObjectMonitor)
  • 特点: 未获取到锁的线程会阻塞(进入等待队列),由操作系统调度,涉及用户态到内核态切换,开销最大

在使用synchronized API的时候,发生了什么事情(锁升级过程全流程)

用一个简单例子说明:

private final Object lock = new Object();

public void method() {
    synchronized (lock) {
        // 临界区代码
    }
}

执行流程:

  1. 创建对象lock时
    对象头Mark Word处于无锁状态(锁标志位01,偏向锁标志位0,表示暂时未启动偏向)

  2. 第一次线程T1进入synchronized块

    • JVM检查偏向锁是否启用(JVM默认启动偏向锁,但有延迟,默认4秒)
    • 如果启用并且没有竞争 → 进入偏向锁模式
      Mark Word中记录T1的线程ID,锁标志位改为偏向锁(01,偏向标志位=1)
    • 此后T1再次进入该同步块,只需检查线程ID是否匹配,不再CAS
  3. 线程T2来竞争锁(此时锁还在T1手中)

    • T2发现Mark Word中已经有T1的线程ID → 偏向锁需要撤销
    • 全局安全点(SafePoint)暂停T1线程,检查T1是否还存活
    • 如果T1已经退出同步块 → 偏向锁撤销,回到无锁状态,然后T2获得偏向锁
    • 如果T1仍在同步块内 → 偏向锁升级为轻量级锁
  4. 轻量级锁的竞争

    • T1和T2都在各自线程栈帧中创建锁记录(Lock Record)
    • 通过CAS尝试将对象头的Mark Word指向自己的锁记录
    • 成功者(T1)持有锁,失败者(T2)自旋(默认自旋次数或自适应自旋)
  5. 自旋一定次数后仍未获得锁 → 锁膨胀为重量级锁

    • JVM为lock对象分配一个ObjectMonitor(包含EntryList、WaitSet等)
    • 未获得锁的线程(T2等)挂起,进入操作系统的阻塞状态
    • 后续所有线程获取锁都需要通过monitor的enter/exit机制

API调用层面的统一性:
无论锁处于什么级别,synchronized代码块在Java源码层面写法完全一样。JVM内部会根据运行时竞争情况自动选择锁策略,对程序员透明。

补充:JVM参数控制锁升级(了解即可)

参数 作用
-XX:+UseBiasedLocking 开启偏向锁(JDK6+默认开启)
-XX:BiasedLockingStartupDelay=0 关闭偏向锁启动延迟
-XX:-UseBiasedLocking 关闭偏向锁,直接进入轻量级锁

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐