写点什么

并发王者课 - 青铜 5:一探究竟 - 如何从 synchronized 理解 Java 对象头中的锁

用户头像
秦二爷
关注
发布于: 2021 年 05 月 27 日
并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁

在前面的文章《青铜 4:synchronized 用法初体验》中,我们已经提到的概念,并指出synchronized是锁机制的一种实现。可是,这么说未免太过抽象,你可能无法直观地理解锁究竟是什么?所以,本文会粗略地介绍synchronized背后的一些基本原理,让你对 Java 中的锁有个粗略但直观的印象。


本文将分两个部分,首先你要从 Mark Word 中认识锁,因为对象锁的信息存在于 Mark Word 中,其次通过 JOL 工具实际体验 Mark Word 的变化。

一、从 Mark Word 认识锁

我们知道,在 HotSpot 虚拟机中,一个对象的存储分布由 3 个部分组成:


  • 对象头(Header):由 Mark Word Klass Pointer 组成;

  • 实例数据(Instance Data):对象的成员变量及数据;

  • 对齐填充(Padding):对齐填充的字节,暂时不必理会。


在这 3 个部分中,对象头中的 Mark Word 是本文的重点,也是理解 Java 锁的关键。Mark Word 记录的是对象运行时的数据,其中包括:


  • 哈希码(identity_hashcode)

  • GC 分代年龄(age)

  • 锁状态标志

  • 线程持有的锁

  • 偏向线程 ID(thread)


所以,从对象头中的 Mark Word 看,Java 中的锁就是对象头中的一种数据。在 JVM 中,每个对象都有这样的锁,并且用于多线程访问对象时的并发控制。


如果一个线程想访问某个对象的实例,那么这个线程必须拥有该对象的。首先,它需要通过对象头中的 Mark Word 判断该对象的实例是否已经被线程锁定。如果没有锁定,那么线程会在 Mark Word 中写入一些标记数据,就是告诉别人:这个对象是我的啦!如果其他线程想访问这个实例的话,就需要进入等待队列,直到当前的线程释放对象的锁,也就是把 Mark Word 中的数据擦除。


当一个线程拥有了锁之后,它便可以多次进入。当然,在这个线程释放锁的时候,那么也需要执行相同次数的释放动作。比如,一个线程先后 3 次获得了锁,那么它也需要释放 3 次,其他线程才可以继续访问。


下面的表格展示的是 64 位计算机中的对象头信息:


|------------------------------------------------------------------------------------------------------------|--------------------||                                            Object Header (128 bits)                                        |        State       ||------------------------------------------------------------------------------|-----------------------------|--------------------||                                  Mark Word (64 bits)                         |    Klass Word (64 bits)     |                    ||------------------------------------------------------------------------------|-----------------------------|--------------------|| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |    OOP to metadata object   |       Normal       ||------------------------------------------------------------------------------|-----------------------------|--------------------|| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |    OOP to metadata object   |       Biased       ||------------------------------------------------------------------------------|-----------------------------|--------------------||                       ptr_to_lock_record:62                         | lock:2 |    OOP to metadata object   | Lightweight Locked ||------------------------------------------------------------------------------|-----------------------------|--------------------||                     ptr_to_heavyweight_monitor:62                   | lock:2 |    OOP to metadata object   | Heavyweight Locked ||------------------------------------------------------------------------------|-----------------------------|--------------------||                                                                     | lock:2 |    OOP to metadata object   |    Marked for GC   ||------------------------------------------------------------------------------|-----------------------------|--------------------|
复制代码


从表格中,你可以看到 Object Header 中的三部分信息:Mark Word、Klass Word、State.

二、通过 JOL 体验 Mark Word 的变化

为了直观感受对象头中 Mark Word 的变化,我们可以通过 JOL(Java Object Layout) 工具演示一遍。JOL 是一个不错的 Java 内存布局查看工具,希望你能记住它。


首先,在工程中引入依赖:


<dependency>    <groupId>org.openjdk.jol</groupId>    <artifactId>jol-core</artifactId>    <version>0.10</version></dependency>
复制代码


在下面的代码中,master是我们创建的对象实例,方法decreaseBlood()中会执行加锁动作。所以,在调用decreaseBlood()加锁后,对象头信息应该会发生变化


 public static void main(String[] args) {        Master master = new Master();        System.out.println("====加锁前====");        System.out.println(ClassLayout.parseInstance(master).toPrintable());        System.out.println("====加锁后====");        synchronized (master) {            System.out.println(ClassLayout.parseInstance(master).toPrintable());        }    }
复制代码


结果输出如下:


====加锁前====cn.tao.king.juc.execises1.Master object internals: OFFSET  SIZE   TYPE DESCRIPTION                               VALUE      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)     12     4    int Master.blood                              100Instance size: 16 bytesSpace losses: 0 bytes internal + 0 bytes external = 0 bytes total
====加锁后====cn.tao.king.juc.execises1.Master object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 48 f9 d6 00 (01001000 11111001 11010110 00000000) (14088520) 4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 int Master.blood 95Instance size: 16 bytesSpace losses: 0 bytes internal + 0 bytes external = 0 bytes total

Process finished with exit code 0
复制代码


从结果中可以看到,代码在执行synchronized方法后,所打印出的object header信息由01 00 00 0000 00 00 00变成了48 f9 d6 0000 70 00 00等等,不出意外的话,相信你应该看不明白这些内容的含义。


所以,为了方便阅读,我们在青铜系列文章《借花献佛-JOL 格式化工具》中提供了一个工具类,让输出更具可读性。借助工具类,我们把代码调整为:


 public static void main(String[] args) {        Master master = new Master();        System.out.println("====加锁前====");        printObjectHeader(master);        System.out.println("====加锁后====");        synchronized (master) {            printObjectHeader(master);        }    }
复制代码


输出的结果如下:


====加锁前====# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scopeClass Pointer: 11111000 00000000 11000001 01000011 Mark Word:  hashcode (31bit): 0000000 00000000 00000000 00000000   age (4bit): 0000  biasedLockFlag (1bit): 0  LockFlag (2bit): 01
====加锁后====Class Pointer: 11111000 00000000 11000001 01000011 Mark Word: javaThread*(62bit,include zero padding): 00000000 00000000 01110000 00000000 00000100 11100100 11101001 100100 LockFlag (2bit): 00
复制代码


你看,这样一来,输出的结果的结果就一目了然。从加锁后的结果中可以看到,Mark Word 已经发生变化,当前线程已经获得对象的锁。


至此,你应该明白,原来 synchronized 的背后的原理是这么回事。当然,本文所讲述只是其中的部分。出于篇幅考虑和难度控制,本文暂且不会对 Java 对象头中锁的含义和锁的升级等问题展开描述,这部分内容会在后面的文章中详细介绍。


以上就是文本的全部内容,恭喜你又上了一颗星✨

夫子的试炼

  • 下载 JOL 工具,在代码中体验工具的使用和对象信息的变化。

关于作者

关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不兜售课程。


如果本文对你有帮助,欢迎点赞关注

发布于: 2021 年 05 月 27 日阅读数: 10
用户头像

秦二爷

关注

微信公众号:【庸人技术笑谈】 2018.05.13 加入

记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不兜售课程。

评论

发布
暂无评论
并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁