美文网首页就该这么学并发
11. 嗨, 需要谈个对象…………?

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

作者: 码哥说 | 来源:发表于2020-08-20 10:07 被阅读0次

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

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
参考文章: 源码架构

欢迎关注我

技术公众号 “CTO技术”

相关文章

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

    嗨, 空闲时间, 谈个对象…………头? 对不起, 咸鱼君又调皮了. 今天, 我要和你谈的不是对象, 而是对象头...

  • GeekBand极客班C++面向对象高级编程(上)第三周笔记

    11.组合与继承 . 遇到复杂问题时,需要类与类相关联,即面向对象思想 Composition复合 . 表示has...

  • 【JS】14--js对象、json对象

    JS对象介绍 前置知识:关于使用对象,可以先阅读一下我的《11.使用对象》这篇文章。 #1.概念 对象是一个包含相...

  • 11.持有对象

    集合类:list(特定顺序) set(元素不重复) queue(一端插入,一端移除) map(键值对) Colle...

  • 11.持有对象

    集合类基本类型:List,Set,Queue,Map 迭代器:是一个对象,用于遍历序列中的对象Iterator:只...

  • 11.持有对象

    1.泛型和类型安全的容器 编译器将不允许你向容器里插入不正确的类型。 2.基本概念 Collection 一个独立...

  • 11.面向对象

    一、实验目的 类 继承 方法重写 对象 属性 二、知识要点 1.类 用来描述具有相同的属性和方法的对象的集合。它定...

  • 一个放松的好方法,当你遇到艰难对话时

    嗨!朋友你好, 你有没有曾经需要进行一场困难对话的时候? 比如说,当你需要跟一个资深的同事谈离职,或是需要跟你的父...

  • 谈“自嗨”

    上午有幸应邀參加由区文联、区作协举行的“文艺名家大课堂”活动,并有幸获赠《一字街》杂志春季卷。 《青春》杂志主编,...

  • 谈对象

    “你谈对象了吗?”随着年龄的增长,越来越多的人频繁地问我这句话,最近尤其是我的家人们开始关注这方面了。 “谈个对象...

网友评论

    本文标题:11. 嗨, 需要谈个对象…………?

    本文链接:https://www.haomeiwen.com/subject/sacmdktx.html