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

并发编程-Synchronized

Mark Word

什么是Mark Word

Mark Word是Java对象头中的一个字段,它是一个32位或64位的字段(取决于系统架构),用于存储对象的元数据信息。这些信息包括对象的哈希码、锁状态、年龄等。

Mark Word有什么用?

Mark Word的主要用途包括:

  1. 存储哈希码:在对象需要计算哈希码时,可以直接从Mark Word中读取,避免重复计算。

  2. 管理锁状态Mark Word用于存储对象的锁状态,支持多种锁机制,如偏向锁、轻量级锁和重量级锁。

  3. 垃圾回收Mark Word中的年龄信息用于垃圾回收,帮助垃圾回收器决定何时回收对象。

Mark Word储存在哪?

下图为图示

Mark Word是Java对象头的一部分,存储在对象的内存布局中。对象的内存布局通常包括以下部分:

  1. Mark Word:存储对象的元数据信息。

  2. Klass Word:存储对象的类信息。

  3. 对象数据:存储对象的实际数据。

在32位系统中,Mark Word是一个32位的字段;在64位系统中,Mark Word是一个64位的字段。为了节省内存,Java在64位系统中使用了一种称为“指针压缩”的技术,将Mark Word压缩为32位。

结构

1. Normal(正常状态)

  • 位布局

    • hashcode:25:25位用于存储对象的哈希码。

    • age:4:4位用于存储对象的年龄(用于垃圾回收)。

    • biased_lock:0:1位用于表示是否启用偏向锁(0表示未启用)。

    • 01:2位用于表示锁的状态(01表示正常状态)。

2. Biased(偏向锁状态)

  • 位布局

    • thread:23:23位用于存储偏向锁的线程ID。

    • epoch:2:2位用于存储偏向锁的纪元信息。

    • age:4:4位用于存储对象的年龄。

    • biased_lock:1:1位用于表示是否启用偏向锁(1表示已启用)。

    • 01:2位用于表示锁的状态(01表示偏向锁状态)。

3. Lightweight Locked(轻量级锁状态)

  • 位布局

    • ptr_to_lock_record:30:30位用于存储指向锁记录的指针。

    • 00:2位用于表示锁的状态(00表示轻量级锁状态)。

4. Heavyweight Locked(重量级锁状态)

  • 位布局

    • ptr_to_heavyweight_monitor:30:30位用于存储指向重量级监视器的指针。

    • 10:2位用于表示锁的状态(10表示重量级锁状态)。

5. Marked for GC(标记为垃圾回收状态)

  • 位布局

    • 11:2位用于表示锁的状态(11表示标记为垃圾回收状态)。

Monitor

Monitor 是什么?

在 Java 中,Monitor(也称为 对象锁内置锁)是 JVM 实现线程同步的核心机制。它用于控制对共享资源的访问,确保在多线程环境下,对共享资源的访问是线程安全的。

一个对象会关联一个monitor

Monitor 的作用

Monitor 的主要作用是:

  • 实现线程同步:通过控制对共享资源的访问,确保同一时间只有一个线程可以访问共享资源。

  • 实现锁机制:Monitor 是 Java 中 synchronized 关键字的底层实现

  • 管理线程等待与唤醒:Monitor 内部维护了多个队列,用于管理等待锁的线程和等待被通知的线程。

  • JVM 统一管理 Monitor:虽然 Monitor 是对象的一部分,但 JVM 会通过全局的机制来管理所有 Monitor 的生命周期。例如,JVM 会维护一个全局的 Monitor 列表,用于分配和回收 Monitor 对象。

  • Monitor 的分配是线程安全的:JVM 会为每个线程维护一个可用的 Monitor 列表(freeused),当线程需要获取锁时,会从这些列表中申请 Monitor 对象。

Monitor 的结构

Monitor 是一个线程私有的数据结构,通常由 JVM 实现(如 HotSpot 虚拟机中的 ObjectMonitor 类)。它包含以下关键部分:

字段说明
_owner指向当前持有锁的线程的唯一标识(如 Thread 对象)
_EntryList等待获取锁的线程(阻塞队列)
_WaitSet等待被通知的线程(等待队列)
_recursions记录锁的重入次数
_count锁的计数器(用于判断是否为可重入锁)

Monitor 的生命周期

  • 创建:当一个对象被 synchronized 加锁时,JVM 会为该对象创建一个 Monitor 对象。

  • 销毁:Monitor 是与对象的生命周期一致的,当对象被垃圾回收时,Monitor 也会被销毁。

  • 统一管理:JVM 会通过全局机制来管理所有 Monitor 的生命周期,包括分配和回收。

总结

问题回答
Monitor 是什么?Monitor 是 Java 中实现线程同步的核心机制,是 synchronized 关键字的底层实现。
Monitor 的作用是什么?Monitor 的作用是实现线程同步,确保对共享资源的访问是线程安全的。
Monitor 的结构是什么?Monitor 包含 _owner_EntryList_WaitSet_recursions 等字段。
Monitor 的存储位置在哪里?Monitor 的地址存储在对象头的 Mark Word 中。
Monitor 是如何被创建和管理的?Monitor 是 JVM 自动创建的,与对象的生命周期一致,由 JVM 统一管理。
Monitor 的作用机制是什么?Monitor 通过 monitorentermonitorexit 实现锁的获取和释放,支持可重入性。
Monitor 的优化机制有哪些?包括偏向锁、轻量级锁和重量级锁等优化机制。

synchronized

是什么

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

就是说 假设现在有两个线程,那么当线程1获取锁在执行的时候,当线程1的时间片用完,但是线程1里面的代码还没有执行完, 那么线程2就不能拿到锁,就会进入等待状态,当线程1执行完临界区代码,释放锁,线程2就会获取锁,继续执行临界区代码。


 

轻量级锁

Mark work布局

  • ptr_to_lock_record:30:30位用于存储指向锁记录的指针。

  • 00:2位用于表示锁的状态(00表示轻量级锁状态)。

轻量级锁是Java中一种用于优化同步操作的锁机制,其主要目的是在没有多线程竞争的情况下,减少锁的开销。轻量级锁的使用场景是当多个线程对同一个对象加锁的时间是错开的(即没有竞争)时,可以使用轻量级锁来优化。这种情况下,线程之间不会发生阻塞,从而提高了性能。

轻量级锁的加锁过程

当一个线程进入同步块时,如果该对象没有被锁定(即锁标志位为“01”状态),虚拟机会在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储对象头中的Mark Word的拷贝。然后,虚拟机会尝试使用CAS操作将对象的Mark Word更新为指向锁记录的指针。

如果更新成功,表示当前线程获得了锁,并且对象的Mark Word的锁标志位会被设置为“00”,表示该对象处于轻量级锁状态。

轻量级锁的解锁过程

当线程退出同步块时,虚拟机会尝试使用CAS操作将当前线程的锁记录替换回对象头。如果替换成功,表示没有竞争发生,同步过程完成;如果替换失败,说明有其他线程尝试获取锁,此时需要唤醒被阻塞的线程。

加锁失败

在轻量级锁的加锁过程中,加锁失败通常有两种情况,而锁重入是其中一种特殊情况。以下是详细分析:

1. 锁已经被其他线程持有(竞争发生)

这是最常见的加锁失败原因。当一个线程在尝试获取轻量级锁时,发现对象的Mark Word已经被其他线程修改为“00”状态(表示轻量级锁状态),说明该对象已经被其他线程加锁,或者当前线程已经尝试过加锁但失败。

  • 原因:多个线程对同一个对象进行同步操作,且加锁时间重叠,导致竞争。

  • 处理方式:轻量级锁会尝试升级为重量级锁,通过操作系统级的互斥量来实现真正的互斥。

2. 锁重入(Reentrant Lock)

锁重入是轻量级锁的一种特殊情况,指的是同一个线程对同一个对象多次加锁。在轻量级锁中,每次重入时,锁记录中会存储一个重入计数器,表示该线程对锁的持有次数。

  • 判断方式:在加锁时,如果发现对象的Mark Word指向的是当前线程的栈帧,说明是锁重入,直接继续执行同步代码即可。

  • 处理方式:在锁重入时,锁记录中的displaced_header会被置为NULL,表示这是一个重入的锁记录。解锁时,如果发现displaced_headerNULL,则说明是锁重入,不需要恢复Mark Word,只需递减重入计数器即可。

重量级锁

Mark Work布局

  • ptr_to_heavyweight_monitor:30:30位用于存储指向重量级监视器的指针。

  • 10:2位用于表示锁的状态(10表示重量级锁状态)。

在多线程环境下,Java中的锁机制会根据竞争情况动态调整锁的状态,从无锁到偏向锁、轻量级锁,最终到重量级锁。当多个线程竞争同一个对象锁时,轻量级锁可能会膨胀为重量级锁。以下是详细的锁膨胀过程:

初始状态

  • 轻量级锁:当一个线程尝试获取对象锁时,如果对象处于无锁状态,线程会在自己的栈帧中创建一个锁记录(Lock Record),并将对象的Mark Word复制到这个锁记录中。然后,线程尝试通过CAS操作将对象的Mark Word更新为指向该锁记录的指针,并将Mark Word的最后两位设置为00,表示轻量级锁状态。

锁膨胀触发条件

  • CAS操作失败:如果在尝试加轻量级锁的过程中,CAS操作无法成功,说明有其他线程已经为该对象加了轻量级锁。此时,当前线程无法通过CAS操作将Mark Word更新为指向自己的锁记录,因此CAS操作会失败。

锁膨胀过程

  • 申请Monitor锁:当CAS操作失败后,线程会进入锁膨胀流程。首先,线程会为对象申请一个Monitor锁,并将对象的Mark Word更新为指向这个Monitor对象的地址。同时,Mark Word的最后两位会被设置为10,表示重量级锁状态。

  • 线程阻塞:由于重量级锁支持阻塞机制,当前线程会被放入Monitor的EntryList中,并进入BLOCKED状态,等待其他线程释放锁。

解锁过程

  • 解锁失败:当持有轻量级锁的线程(Thread-0)退出同步代码块时,它会尝试通过CAS操作将Mark Word恢复为原来的值(即之前保存在锁记录中的值)。然而,由于锁已经膨胀为重量级锁,Mark Word的值已经指向Monitor对象,并且最后两位为10,因此CAS操作会失败。

  • 重量级解锁流程:当CAS操作失败后,线程会进入重量级解锁流程。具体步骤如下:

    • 根据Mark Word中的Monitor地址找到对应的Monitor对象。

    • 将Monitor对象的Owner字段设置为null,表示没有线程持有该锁。

    • 唤醒EntryList中所有被阻塞的线程,让它们有机会竞争锁。

自旋优化 自适应自旋锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞

自旋重试成功的情况

线程 1 (cpu 1 上)对象 Mark线程 2 (cpu 2 上)
-10(重量锁)-
访问同步块,获取 monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行完毕10(重量锁)重量锁指针自旋重试
成功(解锁)01(无锁)自旋重试
-10(重量锁)重量锁指针成功(加锁)
-10(重量锁)重量锁指针执行同步块

自旋锁一般会重试 10 次。这是 Java 中自旋锁的默认设置,由 JVM 内部参数 _spinFreq 控制,表示线程在尝试获取锁失败后,最多会自旋(即循环尝试)10 次。如果在这 10 次尝试中仍未成功获取锁,线程将不再自旋,而是进入阻塞状态,等待锁被释放 。

从 JDK 1.7 开始,自旋锁启用,并且自旋次数由 JVM 动态决定,称为“自适应自旋锁”。这种机制会根据前一次在同一个锁上的自旋时间和锁的持有者状态来调整自旋次数,从而在不同场景下都能达到最佳性能 。

自旋重试失败的情况

线程 1 (cpu 1 上)对象 Mark线程 2 (cpu 2 上)
-10(重量锁)-
访问同步块,获取 monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针阻塞
.........
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • Java 7 之后不能控制是否开启自旋功能

偏向锁

Mark Work布局

  • thread:23:23位用于存储偏向锁的线程ID。

  • epoch:2:2位用于存储偏向锁的纪元信息。

  • age:4:4位用于存储对象的年龄。

  • biased_lock:1:1位用于表示是否启用偏向锁(1表示已启用)。

  • 01:2位用于表示锁的状态(01表示偏向锁状态)。

偏向锁是Java 6中引入的一种锁优化机制,旨在进一步减少锁竞争带来的性能开销。它在无锁竞争的情况下,通过将锁直接偏向于第一个获取它的线程,从而避免了轻量级锁中频繁的CAS(Compare and Swap)操作。

偏向锁的引入背景

在轻量级锁的基础上,JVM发现大多数情况下锁并不会被多个线程竞争,尤其是同一个线程多次获取同一把锁的场景。为了进一步优化这种无竞争情况下的锁性能,JVM引入了偏向锁。偏向锁的核心思想是:将锁的持有者(线程)直接记录在对象头中,后续访问该锁的线程无需再进行CAS操作,从而减少不必要的锁竞争开销。

偏向锁的工作原理

第一次加锁:当第一个线程访问同步代码块时,JVM会通过CAS操作将该线程的ID写入对象头的Mark Word中,并将锁标志位设置为“101”(表示偏向锁状态)。

后续访问:当同一个线程再次访问该同步代码块时,JVM只需检查对象头中的线程ID是否与当前线程一致。如果一致,则直接进入同步代码块,无需再进行CAS操作。

锁重入:如果同一个线程对同一对象多次加锁,JVM会自动处理锁的重入,但每次重入时仍然只需判断线程ID是否匹配,无需执行CAS操作。

轻量级锁和偏向锁图示:

相关文章:

  • WinUI:使用DataGrid控件显示表格
  • 打印机共享问题一键解决,附带设置维护工具
  • 会计-收入-3-关于特定交易的会计处理
  • Power Query动态追加查询(对文件夹下文件汇总)
  • SSM框架实现学生管理系统的需求分析与设计详解
  • 安科瑞亮相2025 SNEC国际太阳能光伏与智慧能源展
  • Mac电脑通过 IntelliJ IDEA 远程连接 MySQL 的详细教程
  • 一个模板元编程示例
  • 亚马逊Woot秒杀:引爆销量
  • Day 48
  • c++动态规划4——环形动态规划
  • 岛屿周长问题的三种解法:直接计数法、数学计算法与深度优先搜索
  • redis-7.4.4使用
  • 论索引影响性能的一面④ 索引失踪之谜【上】
  • 学习日记-day29-6.13
  • python+django/flask成都奥科厨具厂产品在线销售系统
  • Python学习(9) ----- Python的Flask
  • bytes转string
  • icg真的只能用latch不能用Flip-flop吗
  • 洛谷自己创建的一个小比赛【c++】
  • 网站设计费用/网络营销企业网站推广
  • 不要营业执照的做网站/个人网站模板免费下载
  • 重庆网站制作工作室/网上推广赚钱方法
  • 在线网站建设哪家好/seo在线工具
  • 做海报可以在哪些网站下载素材/阿里巴巴logo
  • 青岛专业做网站优化/免费企业建站