11. 嗨, 需要谈个对象…………?

嗨, 空闲时间, 谈个对象…………头?

image

对不起, 咸鱼君又调皮了.

今天, 我要和你谈的不是对象,

image

而是对象头.

没错, 就是Java对象里的对象头知识.

(专业技术文, 阅读需谨慎)

image

前言

上章我们说了synchronized的基本原理, 了解到了synchronized的三种加锁方式

  • 方法锁
  • 类锁
  • 对象锁

抱着往底层深挖的心态, 本章继续深入了解synchronized, 看看在底层, synchronized锁究竟是怎么实现的!

image

为了说清synchronized锁的底层原理,我们得先讲讲两个概念

  • Java对象头

  • 监视器(Monitor)

Java对象头

在JVM中, Java对象在内存中的布局分为三块区域,
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对象头具体结构描述如图:

Java对象头结构组成

Mark Word用于存储对象自身的运行时数据. 如:

  • 哈希码(HashCode)
  • GC分代年龄
  • 锁状态标志
  • 线程持有的锁
  • 偏向线程 ID
  • 偏向时间戳

下图是Java对象头**无锁**状态下Mark Word部分的存储结构(32位虚拟机):
image.png

对象头信息是与对象自身定义的数据无关的**额外存储成本,
考虑到虚拟机的
空间效率,
Mark Word被设计成一个
非固定的数据结构,
以便在
极小的空间内存上存储尽量多的数据,
它会根据对象的状态
复用**自己的存储空间,
也就是说, Mark Word会随着程序的运行发生变化,
可能变化为存储以下4种数据:

Mark Word可能存储4种数据

64位虚拟机下, Mark Word是64bit大小的, 其存储结构如下:
64位Mark Word存储结构

对象头的最后两位存储了**锁的标志位**, 01是初始状态(未加锁) ;
其对象头里存储的是对象本身的哈希码, 随着锁级别的不同, 对象头里会存储不同的内容.

偏向锁存储的是当前占用此对象的线程ID;
而轻量级锁则存储指向线程栈中锁记录的指针;

从这里我们可以看到, “锁”这个东西, 可能是个**锁记录+对象头里的引用指针(判断线程是否拥有锁时, 将线程的锁记录地址和对象头里的指针地址比较);
也可能是
对象头里的线程ID**(判断线程是否拥有锁时, 将线程的ID和对象头里存储的线程ID比较).

HotSpot虚拟机对象头Mark Word

对象头中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对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;

  2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;

  3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

同时, Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),
Synchronized锁便是通过这种方式获取锁的,
这也是Java中任意对象可以作为锁的原因,
同时notify/notifyAll/wait等方法会使用到Monitor锁对象,
所以必须在同步代码块中使用.

监视器Monitor有两种同步方式:

  • 互斥
  • 协作

多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,
监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问.

什么时候需要协作?

举个例子

一个线程向缓冲区写数据, 另一个线程从缓冲区读数据.
如果读线程发现缓冲区为空就会等待,
当写线程向缓冲区写入数据,就会唤醒读线程;
这里读线程和写线程就是一个合作关系.
JVM通过Object类的wait方法来使自己等待,
在调用wait方法后,
该线程会释放它持有的监视器, 直到其他线程通知它才有执行的机会.

一个线程调用notify方法通知在等待的线程,
这个等待的线程并不会马上执行,
而是要通知线程释放监视器后,它重新获取监视器才有执行的机会.
如果刚好唤醒的这个线程需要的监视器被其他线程抢占,
那么这个线程会继续等待.
Object类中的notifyAll方法可以解决这个问题,
它可以唤醒所有等待的线程, 总有一个线程执行.

image.png

如图所示,
一个线程通过1号门进入Entry Set(入口区),
如果在入口区没有线程等待,
那么这个线程就会获取监视器成为监视器的Owner,然后执行监视区域的代码;
如果在入口区中有其它线程在等待,
那么新来的线程也会和这些线程一起等待;
线程在持有监视器的过程中,
有两个选择:

  • 一个是正常执行监视器区域的代码, 释放监视器,通过5号门退出监视器;

  • 还有可能等待某个条件的出现,于是它会通过3号门到Wait Set(等待区)休息, 直到相应的条件满足后再通过4号门进入重新获取监视器再执行;

注意

当一个线程释放监视器时,
在入口区和等待区的等待线程都会去竞争监视器;
如果入口区的线程赢了,会从2号门进入;
如果等待区的线程赢了会从4号门进入;
只有通过3号门才能进入等待区,
在等待区中的线程只有通过4号门才能退出等待区;
也就是说一个线程只有在持有监视器时才能执行wait操作,
处于等待的线程只有再次获得监视器才能退出等待状态.

Bala, Bala,……

希望对各位有所帮助.

如果没有, 请在多读几遍~

若是点个赞, 也是极好的~~

image

参考文章: 源码架构

请关注我的订阅号

订阅号.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码哥说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值