前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于 synchronized 锁优化

关于 synchronized 锁优化

作者头像
BUG弄潮儿
发布2021-02-03 14:27:08
6790
发布2021-02-03 14:27:08
举报
文章被收录于专栏:JAVA乐园

  众所周知,让开发者简单轻松的编写保证线程安全的代码,一直是现代编程语言所最求的,Java 也不例外。Java 语言引入的 synchronized 关键字,无不彰显它在此方面的勃勃雄心。但理想丰满现实骨感,早期的 Java 版本里,对于此关键字的实现太过厚重,导致线程同步的性能远不如预期。Java HotSpot™ VM 经过多个版本的迭代,利用锁膨胀思想,尽量延迟使用重量级锁的手段来提升 synchronized 原语的性能。

对象存储

对象结构

  • 对象 对象在内存中的结构 (64位 JVM)对象头 object header成员变量 object field对齐/填充(可选) alignment/padding gap (optional)
  • 对象数组 对象数组在内存中的结构 (64位 JVM)对象头 object header数组元素字节序列 elements bytes对齐/填充(可选) alignment/padding gap (optional)

  由于对象在内存以 8 字节 为最小单位,单个对象占用内存字节不是 8 的倍数时,需要增加留空字节凑成倍数,此时留空的字节被称为 对象对齐(object alignment)。单个对象内部的成员变量所占字节不满 4 的倍数时,也需增加留空字节凑成倍数,此时的留空字节被称为 填充间隙(padding gap)

对象头结构

  • 对象 对象的 Object Hearder 结构 (64位 JVM)Mark Word (64 位)Klass Word(压缩 32位,不压缩 64位)1对齐/填充(可选) alignment/padding gap (optional)
  • 对象数组 对象数组的 Object Hearder 结构 (64位 JVM)Mark Word (64 位)Klass Word(压缩 32 位,不压缩 64 位)数组长度(32 位)对齐/填充(可选) alignment/padding gap (optional)

Mark Word 结构

  Mark Word,为运行时对象的标记字,字在 32 位系统中占用 32 bit,64 位操作系统占用 64 bit。其记录着对象运行时的数据,包括 identity_hashcodeGC 分代年龄锁状态 等信息。以下引用自 JDK8 HotSpot 源码 markOop.hpp 中,关于 Mark Word 结构信息的描述:

代码语言:javascript
复制
//  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)

  为了节省内存空间,64 位的 JVM 采取压缩普通对象指针 (COOPs,即 Compressed Ordinary Object Pointer) 2 技术,把对象指针由原来的 64 bit 压缩成 32 bit,进而省了一半的内存空间。

  我们着重关注 64 位 JVM 的锁的几种状态,也就是通过上面的 biased_locklock 两个标志位的排列组合得到:

biased_lock

lock

状态

0

01

无锁 normal object

1

01

偏向锁 biased object

0

00

轻量级锁 lightweight/thin object

0

10

重量级锁 heavyweight/fat object

0

11

GC 标记

锁的种类

  根据 Mark Word 中的锁状态,我们分别来介绍下。

重量级锁

  利用系统级别的互斥量(mutex)实现同步临界区,由于系统级调用,开销大,故称其位重量级锁。

  在下一节关于锁的状态改变图中,会发现一个 重量级监视器指针,由于它覆盖(官方称为 Displaced)了原本的 Mark Word,故它所指向的是复杂的数据结构 ObjectMonitor 就包含有用来存储备份的 Mark Word 信息。除此之外,该数据结构还包含锁的等待列表等信息,详细可参考 Monitor Object 设计模式。

  • 优点:持有锁时间长、竞争激烈时,其他等待的线程会让出 CPU 进入等待列表
  • 缺点:系统级调用,开销大

自旋锁

  利用循环的方式来实现线程等待(忙等),等待期间内不让出 CPU 执行时间、免去系统级线程切换开销。

  • 优点:避免线程切换
  • 缺点:等待期间占用 CPU 资源

轻量级锁

  采用原子性的 CAS 进行加解锁操作,加锁失败时自旋等待,成功则将 Mark Word 覆盖成指向线程栈中的 Lock Record 指针,此记录中同样含有原 Mark Word 记录的备份信息。CAS 调用不需系统级别调用,故称为轻量级锁。

  • 优点:无需系统级调用,性能好于重量级锁
  • 缺点:每次加解锁都需要一次 CAS 操作,采用自旋忙等,在竞争激烈、持有锁时间长会加重 CPU 负荷,会转到重量级锁。

偏向锁

  给当前锁标记所属线程,使得所属线程进入同步临界区不用做任何特殊处理,只是简单的使用 CAS 操作将所属线程 ID 记录到 Mark Word 中,同一线程再次加解锁时无需 CAS 操作。

  • 优点:只需初始化时进行一次 CAS 操作,之后加解锁都无需 CAS 操作
  • 缺点:只能单线程无竞争时使用,一旦有其他线程就必须撤销转到轻量级。

锁转态转移

  在给新建的对象分配内存时,其对象头信息会按照下图所示的进行分配,同时随着线程的竞争发送锁状态的转化:

状态转步骤

  具体锁转移的过程如下:

  1. 如果偏向锁机制是启用,那么新建的对象被初始化成匿名的偏向锁。之所以是匿名,是因为此时并没有具体偏向的线程 ID。
    • 使用虚拟机参数 -XX:-UseBiasedLocking 可以关闭偏向锁,默认是启用的。
    • 由于虚拟机参数 -XX:BiasedLockingStartupDelay 默认 4 秒,偏向锁机制会延迟 4 秒生效,测试时可以将其设置为 0 秒,防止偏向锁设置不生效。
    • 调用对象默认的 hashCode()System.identityHashCode(obj) 方法会使偏向锁膨胀为轻量级锁 3。原因为生成的 identity_hashcode 需要被记录到 Mark Word 中,而偏向锁结构没有设计存储备份 Mark Word 的指针,类似轻量级锁的 lock record pointer、重量级锁的 heavyweight monitor pointer
  2. 当有单个线程尝试获取该对象锁时,将把此线程的 ID 写入 Mark Word,完成加锁操作。
  3. 当此单线程解锁时,一直不存在其他线程来竞争时,此时重偏向 (rebias) 匿名偏向锁,即无线程 ID 的偏向锁。
  4. 一旦有其他线程时(不管有无竞争),就撤销偏向 (revoke bias) 转为轻量级锁。
    • 其他线程 CAS 操作将自己的线程号 ID 放入 Mark Word 会失败,此线程会执行撤销偏向,在原偏向锁持有线程到达安全点后暂停原线程,检查原线程是否还持有锁。如果已释放锁,将锁状态修改为普通无锁状态;如果未释放锁,拷贝 Mark Word 到原偏向锁线程的锁记录中,修改锁状态标志位为轻量级,把指向原偏向锁线程的锁记录的指针存入 Mark Word 中,唤醒原持有偏向锁线程。
    • 原持有偏向锁线程继续从安全点之后运行,解锁时判断对象头的锁记录指针是否指向当前线程锁记录、且锁记录的备份 Mark Word 与现有对象头里的 Mark Word 一致,如果都一致说明没有其他线程等待此锁,如果不一致说明有其他线程等待,释放锁之后需要换线挂起的那些线程。
  5. 轻量级锁状态时,如果竞争激烈(等待线程多)、原线程持锁时间长(即其他线程自旋次数多、等待时间长)就会膨胀为重量级锁。
  6. 轻量级、重量级锁解锁后转为普通无锁状态,即后三位为 001
  7. 当然,在重量级锁状态时,如果竞争转为不激烈时,锁会降级为轻量级状态。

状态转移验证源代码

代码依赖
代码语言:javascript
复制
<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-cli</artifactId>
            <version>0.10</version>
        </dependency>
</dependencies>
代码结构
代码语言:javascript
复制
project
└── src
│   └── test
│   │   └── java
│   │   │   └── org
│   │   │   │   └── reion
│   │   │   │   │   └── LockTest.java
│   └── main
│   │   └── resources
│   │   └── java
│   │   │   └── org
│   │   │   │   └── reion
│   │   │   │   │   └── Student.java
代码清单

Student.java

LockTest.java

偏向锁

测试方法

输出结果

注意: 由于 JVM 为 小端法(Little Endian) 故低字节在前,高字节在后。

代码语言:javascript
复制
因此:
    
    打印结果中 object header 中 64 位 Mark Word 字节序列应该为 
    00 00 7f e1 b9 00 a8 05
轻量级锁

测试方法

输出结果

偏向锁 -> 轻量级锁

测试方法

输出结果

轻量级锁维持

测试方法

输出结果

轻量级锁 -> 重量级锁 (高频型)

测试方法

输出结果

轻量级锁 -> 重量级锁 (长时型)

测试方法

输出结果

轻量级锁 -> 重量级锁 -> 轻量级锁

测试方法

输出结果

参考资料

  • Synchronization Created by Christian Wimmer, last modified on Apr 29, 2008
  • Compressed Ordinary Object Pointer
  • markOop.hpp
  • What is in java object header

脚注

  1. 压缩的类指针 (Compressed Class Pointer),使用虚拟机参数 -XX:-UseCompressedClassPointers 关闭压缩,默认开启。
  2. 压缩的普通对象指针 (Compressed Ordinary Object Pointer),使用虚拟机参数 -XX:-UseCompressedOops 关闭压缩,默认开启。
  3. How does the default hashCode() work?

source:https://reionchan.github.io/2020/08/26/about-synchronized-lock-optimization/

喜欢,在看

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 BUG弄潮儿 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 对象存储
    • 对象结构
      • 对象头结构
        • Mark Word 结构
        • 锁的种类
          • 重量级锁
            • 自旋锁
              • 轻量级锁
                • 偏向锁
                • 锁转态转移
                  • 状态转移验证源代码
                    • 代码依赖
                    • 代码结构
                    • 代码清单
                    • 偏向锁
                    • 轻量级锁
                    • 偏向锁 -> 轻量级锁
                    • 轻量级锁维持
                    • 轻量级锁 -> 重量级锁 (高频型)
                    • 轻量级锁 -> 重量级锁 (长时型)
                    • 轻量级锁 -> 重量级锁 -> 轻量级锁
                • 参考资料
                • 脚注
                相关产品与服务
                文件存储
                文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档