嗨, 空闲时间, 谈个对象…………头?
对不起, 咸鱼君又调皮了.
今天, 我要和你谈的不是对象,
而是对象头.
没错, 就是Java对象里的对象头知识.
(专业技术文, 阅读需谨慎)
前言
上章我们说了synchronized的基本原理, 了解到了synchronized的三种加锁方式
- 方法锁
- 类锁
- 对象锁
抱着往底层深挖的心态, 本章继续深入了解synchronized, 看看在底层, synchronized锁究竟是怎么实现的!
为了说清synchronized锁的底层原理,我们得先讲讲两个概念
Java对象头
监视器(Monitor)
Java对象头
在JVM中, Java对象在内存中的布局分为三块区域,
- 对象头
Java对象头一般占有2个机器码
在32位虚拟机中,1个机器码等于4字节, 也就是32bit;
在64位虚拟机中, 1个机器码是8个字节,也就是64bit;但是如果对象是数组类型, 则需要3个机器码,
因为JVM虚拟机虽然可以通过Java对象的元数据信息确定Java对象的大小,
但是无法从数组类型的元数据来确认数组的大小, 所以需要额外使用一块来记录数组长度ps: 元数据是指用来描述数据的数据, 通俗一点,就是描述代码间关系,或者代码与其他资源(例如数据库表)之间内在联系的数据.
- 实例数据
存放类的属性数据信息, 包括父类的属性信息
- 对齐填充
由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的, 仅仅是为了字节对齐
synchronized用的锁就是存在Java对象头里的.
那么什么是Java对象头呢?
Hotspot虚拟机的对象头主要包括两部分数据:
- Mark Word (标记字段)
Mark Word用于
存储对象自身的运行时数据
,它是实现轻量级锁
和偏向锁
的关键.
- Class Pointer (类型指针)
Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.
Java对象头具体结构描述如图:
Mark Word用于存储对象自身的运行时数据. 如:
- 哈希码(HashCode)
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程 ID
- 偏向时间戳
- ...
下图是Java对象头无锁
状态下Mark Word部分的存储结构(32位虚拟机):
对象头信息是与对象自身定义的数据无关的额外存储成本
,
考虑到虚拟机的空间效率
,
Mark Word被设计成一个非固定
的数据结构,
以便在极小的空间内存上存储尽量多的数据
,
它会根据对象的状态复用
自己的存储空间,
也就是说, Mark Word会随着程序的运行发生变化,
可能变化为存储以下4种数据:
64位虚拟机下, Mark Word是64bit大小的, 其存储结构如下:
对象头的最后两位存储了锁的标志位
, 01是初始状态(未加锁)
;
其对象头里存储的是对象本身的哈希码, 随着锁级别的不同, 对象头里会存储不同的内容.
偏向锁存储的是当前占用此对象的线程ID
;
而轻量级锁则存储指向线程栈中锁记录的指针
;
从这里我们可以看到, “锁”这个东西, 可能是个
锁记录+对象头里的引用指针
(判断线程是否拥有锁时, 将线程的锁记录地址和对象头里的指针地址比较);
也可能是对象头里的线程ID
(判断线程是否拥有锁时, 将线程的ID和对象头里存储的线程ID比较).
对象头中Mark Word与线程中Lock Record
在线程进入同步代码块的时候,
如果此同步对象没有被锁定, 即它的锁标志位是01,
则虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)
”的空间, 用于存储锁对象的Mark Word的拷贝,
官方把这个拷贝称为Displaced Mark Word.
整个Mark Word及其拷贝至关重要
.
Lock Record是线程私有
的数据结构,
每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表.
每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),
同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者object mark word), 表示该锁被这个线程占用.
如下图所示为Lock Record的内部结构:
Lock Record | 描述 |
---|---|
Owner | 初始时为NULL, 表示当前没有任何线程拥有该monitor record; 当线程成功拥有该锁后保存线程唯一标识;当锁被释放时又设置为NULL; |
EntryQ | 关联一个系统互斥锁(semaphore), 阻塞所有试图锁住monitor record失败的线程; |
RcThis | 表示blocked或waiting在该monitor record上的所有线程的个数; |
Nest | 用来实现 重入锁的计数; |
HashCode | 保存从对象头拷贝过来的HashCode值(可能还包含GC age). |
Candidate | 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁;如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降.Candidate只有两种可能的值0表示; |
监视器(Monitor)
任何一个对象都有一个Monitor与之关联,
当且一个Monitor被持有后, 它将处于锁定状态.
synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,
虽然具体实现细节不一样,
但是都可以通过成对的MonitorEnter和MonitorExit指令来实现.
- MonitorEnter指令
插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁
- MonitorExit指令
插入在方法结束处和异常处,JVM保证每个-MonitorEnter必须有对应的MonitorExit
那什么是Monitor?
可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象.
在Java的设计中,每一个Java对象自带了一把看不见的锁,
它叫做内部锁或者Monitor锁.
也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,
其中指针指向的是Monitor对象的起始地址.
在Java虚拟机(HotSpot)中, Monitor是由ObjectMonitor实现的,
其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列.
_WaitSet
_EntryList
用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),
_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:
首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
同时, Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),
Synchronized锁便是通过这种方式获取锁的,
这也是Java中任意对象可以作为锁的原因,
同时notify/notifyAll/wait等方法会使用到Monitor锁对象,
所以必须在同步代码块中使用.
监视器Monitor有两种同步方式:
- 互斥
- 协作
多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,
监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问.
什么时候需要协作?
举个例子
一个线程向缓冲区写数据, 另一个线程从缓冲区读数据.
如果读线程发现缓冲区为空就会等待,
当写线程向缓冲区写入数据,就会唤醒读线程;
这里读线程和写线程就是一个合作关系.
JVM通过Object类的wait方法来使自己等待,
在调用wait方法后,
该线程会释放它持有的监视器, 直到其他线程通知它才有执行的机会.一个线程调用notify方法通知在等待的线程,
这个等待的线程并不会马上执行,
而是要通知线程释放监视器后,它重新获取监视器才有执行的机会.
如果刚好唤醒的这个线程需要的监视器被其他线程抢占,
那么这个线程会继续等待.
Object类中的notifyAll方法可以解决这个问题,
它可以唤醒所有等待的线程, 总有一个线程执行.
如图所示,
一个线程通过1号门进入Entry Set(入口区),
如果在入口区没有线程等待,
那么这个线程就会获取监视器成为监视器的Owner,然后执行监视区域的代码;
如果在入口区中有其它线程在等待,
那么新来的线程也会和这些线程一起等待;
线程在持有监视器的过程中,
有两个选择:
一个是正常执行监视器区域的代码, 释放监视器,通过5号门退出监视器;
还有可能等待某个条件的出现,于是它会通过3号门到Wait Set(等待区)休息, 直到相应的条件满足后再通过4号门进入重新获取监视器再执行;
注意
当一个线程释放监视器时,
在入口区和等待区的等待线程都会去竞争监视器;
如果入口区的线程赢了,会从2号门进入;
如果等待区的线程赢了会从4号门进入;
只有通过3号门才能进入等待区,
在等待区中的线程只有通过4号门才能退出等待区;
也就是说一个线程只有在持有监视器时才能执行wait操作,
处于等待的线程只有再次获得监视器才能退出等待状态.
Bala, Bala,……
希望对各位有所帮助.
如果没有, 请在多读几遍~
若是点个赞, 也是极好的~~