とんぼの気持ちとんぼの気持ち
Home
  • Spring Cloud

    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Cloud Netflix
  • Zookeeper

    • Zookeeper
  • 分布式锁

    • 分布式锁
  • 分布式事务

    • 分布式事务
GitHub
Home
  • Spring Cloud

    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Cloud Netflix
  • Zookeeper

    • Zookeeper
  • 分布式锁

    • 分布式锁
  • 分布式事务

    • 分布式事务
GitHub
  • JVM 对高效并发的支持

Java 内存模型

  • 内存模型:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
  • Java 内存模型主要关注 JVM 中把变量值存储到内存中和从内存中取出变量值这样的底层细节
  • 所有共享的变量都存储在主内存中,每个线程都有自己的工作内存,工作内存中保存该线程使用到的变量的主内存副本拷贝
  • 线程对变量的所有操作(读、写)都应该在工作内存中完成
  • 不同线程之间不能相互访问工作内存,交互数据要通过主内存

Snipaste_2023-05-05_15-37-13.png

内存间的交互操作

Java 内存模型规定了一些操作来实现内存间交互,JVM 会保证它们是原子的

交互操作

  • lock:锁定,把变量标识为线程独占,作用于主内存变量
  • unlock:解锁,把锁定的变量释放,别的线程才能使用,作用于主内存变量
  • read:读取,把变量值从主内存读取到工作内存中
  • load:载入,把 read 读取到的主内存中的值放到工作内存的变量副本中
  • use:使用,把工作内存中一个变量的值传递给执行引擎(如:字节码执行引擎)
  • assign:赋值,把从执行引擎接收到的值赋给工作内存里面的变量
  • store:存储,把工作内存中一个变量的值传递到主内存中
  • write:写入,把 store 进来的数据存储到如主内存的变量中

Snipaste_2023-05-05_15-57-46.png

交互操作的规则

  • 不允许 read 和 load、store 和 write 操作之一单独出现,以上两个操作必须按顺序执行,但不保证连续执行,即,read 和 load 之间、store 和 write 之间可以插入其他指令
  • 不允许一个线程丢弃它的最近的 assign 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存中
  • 不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中
  • 一个新的公关变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的变量,也就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作
  • 一个变量在同一个时刻只运行一个线程对其执行 lock 操作,但 lock 操作可以被同一个线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁
  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值
  • 如果一个变量没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不能 unlock 一个被其他线程锁定的变量
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存(执行 store 和 write 操作)

多线程的可见性

  • 可见性:一个线程修改了变量,其他线程可以获取到被修改后的值
  • 保证可见性的常见方法:volatile、synchronized、final(一旦初始化完成,其他线程就可见)

volatile

  • volatile 是 JVM 提供的最轻量级的同步机制,用 volatile 修饰的变量,对所有的线程可见,即对 volatile 变量所做的写操作能立即反映到其他线程中
  • 用 volatile 修饰的变量,在多线程环境下仍然是不安全的
  • volatile 修饰的变量,禁止指令重排优化

适用 volatile 的场景

  • 运算结果不依赖变量的当前值
  • 或者能确保只有一个线程修改变量的值

有序性

  • 在本线程内,操作都是有序的
  • 在线程外观察,操作都是无序的,因此存在指令重排或主内存同步延时

指令重排

  • 指令重排:指的是 JVM 为了优化,在条件允许的情况下,对指令进行一定的重新排列,直接运行当前能够立即执行的后续指令,避开获取下一条指令所需数据造成的等待
  • 线程内串行语义(先后顺序),不考虑多线程间的语义
  • 不是所有的指令都能重排,如:
    • 写后读:a = 1;b = a;写一个变量后,再读取这个变量
    • 写后写:a = 1;a = 2;写一个变量后,再写这个变量
    • 读后写:a = b;b = 1;读取一个变量后,再写这个变量

基本规则

  • 程序顺序原则:一个线程内保证语义的串行性(即代码是在一个线程中运行的,则按照先后顺序执行)
  • volatile 规则:volatile 变量一定要先赋值后才能取值
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
  • 传递性:A 先于 B,B 先于 C,那么 A 必然先于 C
  • 线程的 start() 方法先于它的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数执行结束先于 finalize() 方法

线程安全的处理方法

  • 不可变是线程安全的
  • 互斥同步(阻塞同步):synchronized、ReentrantLock
    • 一般情况下,synchronized 的性能比 ReentrantLock 要好
    • ReentrantLock 具有一些 synchronized 不具备的特性
      • 等待可中断:当持有锁的线程长时间不释放锁,正在等待的线程可以选择放弃等待
      • 公平锁:多个线程等待同一个锁时,必须严格按照申请锁的时间顺序来获取锁
      • 可以绑定多个条件:一个 ReentrantLock 对象可以绑定多个 condition 对象,而 synchronized 是针对一个条件的,如果要针对多个条件就需要多个锁
  • 非阻塞同步:是一种基于冲突检查的乐观锁策略,通常是先操作,如果没有冲突,操作就成功了,有冲突再采取其他方式进行补偿处理
  • 无同步方案:在多线程中方法不涉及共享数据时不需要同步

锁优化

自旋锁

  • 自旋:如果线程可以很快获取锁,那么可以不在 OS 层挂起线程,而是让线程做几次循环,即自旋
  • 自适应自旋:自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间和锁的拥有者状态来决定
  • 如果锁被占用时间很短,自旋成功,那么能节省线程挂起、切换的时间,从而提升系统性能
  • 如果锁被占用的时间很长,自旋失败,会耗费处理器资源,降低系统性能

锁消除

  • 在编译代码的时候,JVM 检测到代码不存在共享数据竞争,就无需同步加锁,JVM 会将加的锁消除掉
  • 通过 -XX:+EliminateLocks 开启,同时需要使用 -XX:+DoEscapeAnalysis 开启逃逸分析
  • 逃逸分析
    • 方法逃逸:一个方法中定义的一个对象,可能被外部方法引用
    • 线程逃逸:对象可能被其他外部线程访问,如:赋值给类变量、在其他线程中访问实例变量
  • 可逃逸的资源不能进行锁消除

锁粗化

  • 通常要求同板块要小,但一系列连续的操作导致对一个对象反复加锁和解锁,这会导致不必要的性能损耗,这种情况建议把锁同步的范围加大到整个操作上

轻量级锁

  • 轻量级是相对于传统锁机制而言,本意是没有多线程竞争的情况下,减少传统锁机制使用 OS 实现的互斥锁产生的性能损耗
  • 实现方式类似乐观锁
  • 轻量级锁失败,表示存在竞争,升级为重量级锁,导致性能下降

偏向锁

  • 偏向锁是在无竞争情况下,直接把整个同步消除,连乐观锁都不需要,从而提高性能,锁会偏向于当前已经占有锁的线程
  • 只要没有竞争,获取偏向锁的线程,在将来进入同步块,也不需要做同步
  • 当有其他线程请求相同的锁时,偏向模式结束
  • 如果程序中大多数锁总是被多个线程访问的时候,即竞争比较激烈,偏向锁会降低性能
  • 使用 -XX:-UseBiasedLocking 来禁用偏向锁,默认是开启的

JVM 获取锁的步骤

  1. 先尝试偏向锁
  2. 然后尝试轻量级锁
  3. 再然后尝试自旋锁
  4. 最后尝试普通锁,使用 OS 互斥量在操作系统层挂起

大致流程

  1. 尝试获取锁对象的对象头:当一个线程需要获取一个对象的锁时,首先会尝试获取这个对象的对象头,这个过程是比较轻量级的操作。
  2. 判断锁对象的对象头是否已经被其他线程占用:如果锁对象的对象头已经被其他线程占用了,那么当前线程就会进入锁阻塞状态。
  3. 自旋获取锁:如果锁对象的对象头没有被其他线程占用,当前线程就会尝试自旋获取锁。自旋是指当前线程在一个循环中反复尝试获取锁,直到获取到锁或者等待超时。
  4. 尝试获取锁失败,进入锁阻塞状态:如果自旋获取锁的过程中等待超时或者被其他线程抢占了锁,当前线程就会进入锁阻塞状态,等待锁释放或者被唤醒。

JVM 对高效并发的支持

1. Java 内存模型(JMM)

1.1 内存模型### 2.2 操作规则

2.2.1 基本约束

  1. 配对执行:不允许 read 和 load、store 和 write 操作之一单独出现,必须成对执行
  2. 同步一致性:不允许一个线程丢弃它最近的 assign 操作,即变量在工作内存中改变后必须同步回主内存
  3. 无因变更禁止:不允许一个线程无原因地把数据从工作内存同步回主内存
  4. 初始化要求:新的变量只能在主内存中"诞生",不允许在工作内存中直接使用未初始化的变量

2.2.2 锁定规则

  1. 独占性:一个变量在同一时刻只允许一个线程对其执行 lock 操作
  2. 重入性:lock 操作可以被同一个线程重复执行多次,对应需要执行相同次数的 unlock 操作
  3. 清空机制:执行 lock 操作时,将清空工作内存中此变量的值,需要重新执行 load 或 assign 操作初始化
  4. 解锁前同步:对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存

3. 并发特性保障

3.1 可见性(Visibility)

3.1.1 可见性定义

可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

3.1.2 可见性保障机制

  • volatile 关键字:保证修饰的变量对所有线程立即可见
  • synchronized 关键字:进入同步块前会清空工作内存,退出时会将变量同步到主内存
  • final 关键字:一旦初始化完成,其他线程就能看到 final 字段的值
  • Lock 接口:与 synchronized 类似的可见性保障
  • 线程启动和结束:Thread.start()和 Thread.join()等操作具有可见性语义(Java Memory Model,JMM)**:
  • 定义:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
  • 作用:规定了 JVM 中变量值在内存中的存储和读取的底层细节
  • 目标:解决多线程环境下的可见性、有序性和原子性问题

1.2 内存架构

1.2.1 主内存与工作内存

  • 主内存(Main Memory):

    • 存储所有共享变量的主要区域
    • 对应物理内存的一部分
    • 所有线程共享访问
  • 工作内存(Working Memory):

    • 每个线程私有的内存空间
    • 保存该线程使用到的变量的主内存副本
    • 线程对变量的所有操作都在工作内存中进行

1.2.2 交互规则

  • 线程对变量的所有操作(读取、赋值)必须在工作内存中进行
  • 不同线程之间不能直接访问对方的工作内存
  • 线程间变量值的传递需要通过主内存来完成

2. 内存间交互操作

2.1 八种原子操作

Java 内存模型规定了 8 种原子操作来实现主内存与工作内存间的交互,JVM 保证每一种操作都是原子的:

2.1.1 主内存操作

  • lock(锁定):作用于主内存变量,把一个变量标识为线程独占状态
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中
  • write(写入):作用于主内存变量,把 store 操作从工作内存中得到的变量值放入主内存的变量中

2.1.2 工作内存操作

  • load(载入):作用于工作内存变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存变量,把工作内存中的一个变量值传递给执行引擎
  • assign(赋值):作用于工作内存变量,把从执行引擎接收到的值赋值给工作内存的变量
  • store(存储):作用于工作内存变量,把工作内存中的一个变量值传送到主内存中

![内存交互操作示意图]

交互操作的规则

  • 不允许 read 和 load、store 和 write 操作之一单独出现,以上两个操作必须按顺序执行,但不保证连续执行,即,read 和 load 之间、store 和 write 之间可以插入其他指令
  • 不允许一个线程丢弃它的最近的 assign 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存中
  • 不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中
  • 一个新的公关变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的变量,也就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作
  • 一个变量在同一个时刻只运行一个线程对其执行 lock 操作,但 lock 操作可以被同一个线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁
  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值
  • 如果一个变量没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不能 unlock 一个被其他线程锁定的变量
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存(执行 store 和 write 操作)

多线程的可见性

  • 可见性:一个线程修改了变量,其他线程可以获取到被修改后的值
  • 保证可见性的常见方法:volatile、synchronized、final(一旦初始化完成,其他线程就可见)

3.2 volatile 关键字详解

3.2.1 volatile 特性

  • 可见性保证:对 volatile 变量的写操作会立即刷新到主内存,读操作会从主内存中获取最新值
  • 禁止指令重排序:volatile 变量的读写不会被重排序到其他内存操作之前或之后
  • 不保证原子性:volatile 变量的复合操作(如 i++)仍然不是原子的

3.2.2 volatile 适用场景

  1. 状态标志:用于标识程序状态的布尔变量
  2. 一次性安全发布:确保对象的安全初始化
  3. 独立观察:多个线程观察同一个变量的变化
  4. 配合原子操作:与原子类配合使用

适用条件:

  • 运算结果不依赖变量的当前值
  • 或者能确保只有单个线程修改变量的值
  • 变量不需要与其他状态变量共同参与不变约束

3.2.3 volatile 实现原理

  • 内存屏障:在 volatile 变量读写前后插入内存屏障指令
  • 缓存一致性协议:利用 CPU 的缓存一致性机制保证可见性
  • 编译器优化限制:防止编译器对 volatile 变量进行优化

3.3 有序性(Ordering)

3.3.1 有序性定义

有序性:程序执行的顺序按照代码的先后顺序执行。

3.3.2 有序性问题来源

  • 单线程内:操作都是有序的(as-if-serial 语义)
  • 多线程间:由于指令重排序和内存同步延迟,操作可能呈现无序
  • 编译器优化:编译器可能调整指令顺序以提高性能
  • 处理器优化:CPU 可能乱序执行指令

3.4 指令重排序

3.4.1 指令重排序定义

指令重排序:JVM 为了优化性能,在不改变程序执行结果的前提下,对指令执行顺序进行重新安排。

3.4.2 重排序类型

  1. 编译器重排序:编译器在不改变单线程程序语义的前提下可以重新安排语句的执行顺序
  2. 指令级并行重排序:现代处理器采用指令级并行技术来将多条指令重叠执行
  3. 内存系统重排序:由于处理器使用缓存和读写缓冲区,使得加载和存储操作看上去可能是乱序执行

3.4.3 数据依赖性

不能重排序的情况:

  • 写后读:a = 1; b = a; 写一个变量后,再读取这个变量
  • 写后写:a = 1; a = 2; 写一个变量后,再写这个变量
  • 读后写:a = b; b = 1; 读取一个变量后,再写这个变量

3.4.4 happens-before 原则

JMM 使用 happens-before 关系来阐述操作之间的内存可见性:

  1. 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作
  2. 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁
  3. volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读
  4. 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C
  5. 线程启动规则:Thread 对象的 start()方法 happens-before 于此线程的每一个动作
  6. 线程终止规则:线程中的所有操作都 happens-before 于对此线程的终止检测
  7. 线程中断规则:对线程 interrupt()方法的调用 happens-before 于被中断线程的代码检测到中断事件的发生
  8. 对象终结规则:一个对象的初始化完成 happens-before 于它的 finalize()方法的开始

4. 线程安全实现方法

4.1 线程安全分类

4.1.1 不可变对象

  • 定义:对象一旦创建,其状态就不能被修改
  • 实现方式:
    • 使用 final 关键字修饰类和字段
    • 不提供任何修改对象状态的方法
    • 确保所有字段都是 private final 的
  • 典型例子:String、Integer、Long 等包装类
  • 优势:天然线程安全,无需同步机制

4.1.2 绝对线程安全

  • 定义:不管运行时环境如何,调用者都不需要任何额外的同步措施
  • 实现成本:通常需要付出很大的性能代价
  • 实际应用:在 Java 中很少有真正绝对线程安全的类

4.1.3 相对线程安全

  • 定义:通常意义上的线程安全,对对象的单独操作是线程安全的
  • 典型例子:Vector、HashTable、Collections.synchronizedList()等
  • 注意事项:复合操作仍需要额外同步

4.1.4 线程兼容

  • 定义:对象本身不是线程安全的,但可以通过同步手段在并发环境中安全使用
  • 典型例子:ArrayList、HashMap 等大部分 Java 类
  • 使用方式:需要用户自己保证同步

4.1.5 线程对立

  • 定义:无论是否采取同步措施,都无法在多线程环境中并发使用
  • 典型例子:System.setIn()、System.setOut()等

4.2 同步机制

4.2.1 互斥同步(阻塞同步)

synchronized 关键字:

  • 作用范围:可以修饰方法、代码块
  • 锁对象:
    • 实例方法:锁定当前实例对象
    • 静态方法:锁定当前 Class 对象
    • 代码块:锁定指定对象
  • 特点:重量级锁,会导致线程阻塞

ReentrantLock:

  • 可重入性:同一个线程可以多次获得同一把锁
  • 公平性:支持公平锁和非公平锁
  • 可中断性:支持响应中断
  • 条件变量:支持多个条件变量
  • 灵活性:提供 tryLock()等更灵活的锁获取方式

4.2.2 非阻塞同步

CAS(Compare-And-Swap)操作:

  • 原理:基于硬件提供的原子性操作,比较内存位置的值与预期值,如果匹配则更新为新值
  • 优势:无需阻塞线程,性能较好
  • 问题:
    • ABA 问题:值被改变后又改回原值
    • 循环时间长开销大:竞争激烈时会消耗大量 CPU 资源
    • 只能保证一个共享变量的原子操作

原子类:

  • 基本类型原子类:AtomicInteger、AtomicLong、AtomicBoolean
  • 数组类型原子类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • 引用类型原子类:AtomicReference、AtomicStampedReference、AtomicMarkableReference
  • 字段更新器原子类:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

4.2.3 无同步方案

线程本地存储(ThreadLocal):

  • 原理:为每个线程分配独立的变量副本
  • 使用场景:
    • 保存线程上下文信息
    • 跨层传递参数
    • 线程安全的 SimpleDateFormat
  • 注意事项:需要及时清理避免内存泄漏

可重入代码(纯函数):

  • 特点:不依赖存储在堆上的数据和公用的系统资源
  • 要求:不调用非可重入的方法,只使用栈上分配的变量

栈封闭:

  • 原理:多个线程访问同一个方法的局部变量时,不会出现线程安全问题
  • 原因:局部变量存储在虚拟机栈中,属于线程私有

5. 锁优化技术

5.1 JVM 锁优化策略

5.1.1 自旋锁(Spin Lock)

基本原理:

  • 当线程尝试获取锁失败时,不立即阻塞,而是循环检查锁状态
  • 适用于锁持有时间很短的场景
  • 避免了线程上下文切换的开销

自适应自旋:

  • 自旋时间不再固定,而是动态调整
  • 基于前一次在同一个锁上的自旋时间和锁拥有者状态决定
  • 如果自旋成功,下次自旋时间会更长;如果自旋失败,自旋时间会缩短

优缺点:

  • 优点:减少上下文切换,提高性能
  • 缺点:在锁竞争激烈时会浪费 CPU 资源

5.1.2 锁消除(Lock Elimination)

实现原理:

  • JIT 编译器通过逃逸分析判断同步块是否真正需要同步
  • 如果确定没有线程安全问题,会自动消除锁
  • 需要开启逃逸分析:-XX:+DoEscapeAnalysis
  • 开启锁消除:-XX:+EliminateLocks

逃逸分析类型:

  • 方法逃逸:对象被外部方法引用
  • 线程逃逸:对象可能被其他线程访问

适用场景:

  • 局部对象的同步操作
  • 字符串拼接等场景

5.1.3 锁粗化(Lock Coarsening)

基本思想:

  • 将多个连续的锁操作合并为一个范围更大的锁操作
  • 减少锁操作的开销
  • 适用于频繁获取和释放同一把锁的场景

典型场景:

// 优化前:多次获取释放锁
for (int i = 0; i < 100; i++) {
    synchronized (obj) {
        // 操作
    }
}

// 优化后:扩大锁范围
synchronized (obj) {
    for (int i = 0; i < 100; i++) {
        // 操作
    }
}

5.2 锁升级机制

5.2.1 偏向锁(Biased Lock)

设计思想:

  • 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得
  • 消除无竞争情况下的同步原语,进一步提高程序性能

工作原理:

  • 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID
  • 后续该线程再次进入和退出同步块时不需要进行 CAS 操作来加锁和解锁

撤销条件:

  • 其他线程尝试获取锁
  • 调用对象的 wait()方法
  • 调用对象的 hashCode()方法

控制参数:

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

5.2.2 轻量级锁(Lightweight Lock)

使用场景:

  • 在无多线程竞争的情况下,减少传统重量级锁的性能损耗
  • 基于 CAS 操作实现

工作流程:

  1. 线程在执行同步块之前,JVM 会在当前线程的栈帧中创建用于存储锁记录的空间
  2. 将对象头中的 Mark Word 复制到锁记录中
  3. 线程尝试使用 CAS 将对象头的 Mark Word 替换为指向锁记录的指针
  4. 如果成功,当前线程获得锁;如果失败,表示有其他线程竞争锁,锁会膨胀为重量级锁

特点:

  • 加锁和解锁都是通过 CAS 操作完成
  • 没有多线程竞争时,避免了互斥量的开销
  • 有竞争时会膨胀为重量级锁

5.2.3 重量级锁(Heavyweight Lock)

特征:

  • 基于操作系统的互斥量(Mutex)实现
  • 需要在用户态和内核态之间切换
  • 会导致线程阻塞和唤醒

适用场景:

  • 锁竞争激烈的情况
  • 线程持锁时间较长的情况

5.3 锁状态转换流程

无锁状态 → 偏向锁 → 轻量级锁 → 重量级锁

转换过程:

  1. 对象创建:对象处于无锁状态
  2. 首次加锁:如果启用偏向锁且无竞争,升级为偏向锁
  3. 出现竞争:偏向锁撤销,升级为轻量级锁
  4. 竞争激烈:轻量级锁膨胀为重量级锁

注意事项:

  • 锁升级是单向的,不会降级
  • 每种锁都有其适用的场景
  • JVM 会根据实际情况自动选择最合适的锁实现

6. JVM 锁获取机制

6.1 锁获取的完整流程

6.1.1 获取锁的步骤

  1. 检查对象头:检查对象头的 Mark Word,判断当前锁状态
  2. 尝试偏向锁:如果是无锁状态且启用偏向锁,尝试获取偏向锁
  3. 尝试轻量级锁:如果偏向锁获取失败或被撤销,尝试轻量级锁
  4. 尝试自旋锁:轻量级锁获取失败时,进行自旋等待
  5. 膨胀为重量级锁:自旋失败后,锁膨胀为重量级锁,线程被阻塞

6.1.2 详细执行流程

第一阶段:对象头检查

  • 检查对象头中的 Mark Word 标志位
  • 判断当前是否已经加锁以及锁类型
  • 如果是当前线程持有的偏向锁,直接获取成功

第二阶段:偏向锁获取

  • 如果对象处于无锁状态且开启偏向锁优化
  • 使用 CAS 操作将线程 ID 设置到对象头
  • 成功则获取偏向锁,失败则撤销偏向锁

第三阶段:轻量级锁获取

  • 在线程栈中创建锁记录(Lock Record)
  • 复制对象头的 Mark Word 到锁记录
  • 使用 CAS 操作将对象头指向锁记录
  • 成功则获取轻量级锁

第四阶段:自旋等待

  • 如果轻量级锁获取失败,开始自旋
  • 循环检查锁状态,尝试获取锁
  • 自旋次数有限制,避免无限循环

第五阶段:重量级锁

  • 自旋失败后,锁膨胀为重量级锁
  • 线程进入阻塞状态,由操作系统调度
  • 锁释放时会唤醒等待的线程

6.2 锁释放机制

6.2.1 偏向锁释放

  • 偏向锁的释放不需要特殊操作
  • 只有在其他线程竞争时才会撤销偏向锁
  • 撤销过程需要等待安全点(SafePoint)

6.2.2 轻量级锁释放

  • 使用 CAS 操作将锁记录中的 Mark Word 替换回对象头
  • 如果替换成功,锁释放完成
  • 如果替换失败,说明锁已膨胀,需要释放重量级锁

6.2.3 重量级锁释放

  • 释放 Monitor 锁
  • 唤醒等待队列中的线程
  • 被唤醒的线程重新竞争锁

6.3 性能优化建议

6.3.1 减少锁竞争

  • 减少锁持有时间:缩小同步代码块范围
  • 减少锁粒度:使用分段锁或读写锁
  • 使用无锁算法:如 CAS 操作、ThreadLocal 等
  • 避免不必要的同步:如不可变对象、局部变量等

6.3.2 锁优化策略

  • 选择合适的同步机制:根据场景选择 synchronized、ReentrantLock、原子类等
  • 避免死锁:按顺序获取锁,使用 tryLock()设置超时
  • 监控锁性能:使用 JVM 参数开启锁统计信息

6.3.3 JVM 参数调优

# 偏向锁相关
-XX:+UseBiasedLocking          # 启用偏向锁(默认)
-XX:BiasedLockingStartupDelay=0 # 立即启用偏向锁

# 自旋锁相关
-XX:+UseSpinning               # 启用自旋锁(JDK 6默认开启)
-XX:PreBlockSpin=10            # 自旋次数

# 锁消除相关
-XX:+EliminateLocks           # 启用锁消除
-XX:+DoEscapeAnalysis         # 启用逃逸分析

# 监控相关
-XX:+PrintConcurrentLocks     # 打印并发锁信息
-XX:+PrintGCApplicationStoppedTime # 打印停顿时间

7. 总结

Java 虚拟机通过内存模型、锁优化、同步机制等多个层面为高效并发提供了全面支持:

  1. 内存模型:通过 JMM 规范保证了多线程环境下的可见性、有序性和原子性
  2. 同步机制:提供了从 synchronized 到 Lock 接口的多种同步工具
  3. 锁优化:实现了从偏向锁到重量级锁的自适应升级机制
  4. 无锁编程:支持 CAS 操作和原子类,提供了高性能的无锁解决方案

理解这些机制有助于开发者编写高性能的并发程序,并在遇到性能问题时能够有针对性地进行优化。

Edit this page
最后更新时间:
贡献者: hyfly233