并发王者课-青铜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                              100
Instance size: 16 bytes
Space 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                              95
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Process finished with exit code 0
从结果中可以看到,代码在执行synchronized方法后,所打印出的object header信息由01 00 00 00、00 00 00 00变成了48 f9 d6 00、00 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_scope
Class 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工具,在代码中体验工具的使用和对象信息的变化。
 
关于作者
关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不兜售课程。
如果本文对你有帮助,欢迎点赞、关注。
并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁的更多相关文章
- 并发王者课 - 青铜4:synchronized用法初体验
		
在前面的文章<双刃剑-理解多线程带来的安全问题>中,我们提到了多线程情况下存在的线程安全问题.本文将以这个问题为背景,介绍如何通过使用synchronized关键字解这一问题.当然,在青铜 ...
 - 并发王者课 - 青铜 2:峡谷笔记 - 简单认识Java中的线程
		
在前面的<兵分三路:如何创建多线程>文章中,我们已经通过Thread和Runnable直观地了解如何在Java中创建一个线程,相信你已经有了一定的体感.在本篇文章中,我们将基于前面的示例代 ...
 - 并发王者课-青铜7:顺藤摸瓜-如何从synchronized中的锁认识Monitor
		
在前面的文章中,我们已经体验过synchronized的用法,并对锁的概念和原理做了简单的介绍.然而,你可能已经察觉到,有一个概念似乎总是和synchronized.锁这两个概念如影相随,很多人也比较 ...
 - 并发王者课-铂金6:青出于蓝-Condition如何把等待与通知玩出新花样
		
欢迎来到<[并发王者课](https://juejin.cn/post/6967277362455150628)>,本文是该系列文章中的**第19篇**. 在上一篇文章中,我们介绍了阻塞队 ...
 - 并发王者课-铂金2:豁然开朗-“晦涩难懂”的ReadWriteLock竟如此妙不可言
		
欢迎来到<并发王者课>,本文是该系列文章中的第15篇. 在上篇文章中,我们介绍了Java中锁的基础Lock接口.在本文中,我们将介绍Java中锁的另外一个重要的基本型接口,即ReadWri ...
 - 并发王者课-铂金1:探本溯源-为何说Lock接口是Java中锁的基础
		
欢迎来到<并发王者课>,本文是该系列文章中的第14篇. 在黄金系列中,我们介绍了并发中一些问题,比如死锁.活锁.线程饥饿等问题.在并发编程中,这些问题无疑都是需要解决的.所以,在铂金系列文 ...
 - 并发王者课-铂金8:峡谷幽会-看CyclicBarrier如何跨越重峦叠嶂
		
欢迎来到<并发王者课>,本文是该系列文章中的第21篇,铂金中的第8篇. 在上一篇文章中,我们介绍了CountDownLatch的用法.在协调多线程的开始和结束时,CountDownLatc ...
 - 并发王者课-铂金9:互通有无-Exchanger如何完成线程间的数据交换
		
欢迎来到<并发王者课>,本文是该系列文章中的第22篇,铂金中的第9篇. 在前面的文章中,我们已经介绍了ReentrantLock,CountDownLatch,CyclicBarrier, ...
 - 并发王者课-铂金10:能工巧匠-ThreadLocal如何为线程打造私有数据空间
		
欢迎来到<并发王者课>,本文是该系列文章中的第23篇,铂金中的第10篇. 说起ThreadLocal,相信你对它的名字一定不陌生.在并发编程中,它有着较高的出场率,并且也是面试中的高频面试 ...
 
随机推荐
- 我与Git的那些破事(上)--代码管理
			
1. Git是什么? 作为一名程序猿,我相信大家都或多或少接触过git--分布式版本控制软件. 有人说,它是目前世界上最先进的分布式版本控制系统,我想说,是否最先进不知道,但确实好用,实用. 作为一款 ...
 - JavaScript深拷贝与浅拷贝的区别和实现方式
			
如何区分深拷贝和浅拷贝呢,简单来说对象B拷贝了对象A,如果对象A和对象B共用一个对象,对象B改变对象A跟着改变这就是浅拷贝:但如果对象B拷贝了对象A,但是对象A和对象B是分开的,那么就是深拷贝 基本数 ...
 - mongodb数据修复宝典
			
mongodb数据修复宝典 1. 版本信息 mongodb版本: 3.6.14 2.mongodb日志内容: 2019-10-04T10:41:38.558+0000 E STORAGE [inita ...
 - IPC 方法分类
			
IPC 方法分类 进程间通信 shell out 被调用程序在执行完毕之前接管用户的键盘和显示,退出后,调用程序重新控制键盘和显示并继续运行. 专门程序通常有文件系统与父进程进行通信,方法是在指定位置 ...
 - 【luogu P3803】【模板】多项式乘法(FFT)
			
[模板]多项式乘法(FFT) 题目链接:luogu P3803 题目大意 给你两个多项式,要你求这两个多项式乘起来得到的多项式.(卷积) 思路 系数表示法 就是我们一般来表示一个多项式的方法: \(A ...
 - nginx下强制跳转到www域名
			
跳转www #先监听 exp.com域名,然后转发到www下面 server { listen 80; server_name exp.com; rewrite ^(.*) $scheme://www ...
 - 常用head标签
			
最小推荐 <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content= ...
 - TP5学习记录(Model篇)
			
ThinkPHP 数据库操作 数据库连接 #在config/database.php设置数据库连接参数或者利用Db::connect()方法设置数据库连接 /* * public static fun ...
 - Linux中su、sudo、sudo -i的用法和区别
			
sudo :暂时切换到超级用户模式以执行超级用户权限,提示输入密码时该密码为当前用户的密码,而不是超级账户的密码.缺点是每次执行超级用户权限都要在命令前加上 sudo ,优点是在当前终端再使用 sud ...
 - JVM虚拟机-了解Java堆中对象分配、布局和访问的全过程
			
目录 前言 对象的创建 类加载检查 分配内存 内存空间分配方式 指针碰撞 空闲列表 并发时的内存分配 同步处理:CAS 本地线程分配缓冲:TLAB 初始化零值 设置对象头 执行 init 方法 对象的 ...