就该这么学并发

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

2020-08-20  本文已影响0人  码哥说

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

image

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

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

image

而是对象头.

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

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

image

前言

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

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

image

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

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用于存储对象自身的运行时数据,它是实现轻量级锁偏向锁的关键.

Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.

Java对象头具体结构描述如图:

Java对象头结构组成

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

下图是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指令来实现.

插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁

插入在方法结束处和异常处,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中有两个队列.

用来保存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,然后执行监视区域的代码;
如果在入口区中有其它线程在等待,
那么新来的线程也会和这些线程一起等待;
线程在持有监视器的过程中,
有两个选择:

注意

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

Bala, Bala,……

希望对各位有所帮助.

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

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

image
参考文章: 源码架构

欢迎关注我

技术公众号 “CTO技术”

上一篇 下一篇

猜你喜欢

热点阅读