2021-06-01 深入分析锁的基础知识

目录

 

一 对象在内存中的布局? 创建一个Object对象占用多少内存?

1.1 对象在内存中的布局?

1.2 创建一个Object对象占用多少内存?

二Safepoint(安全点)?

2.1 什么是safepoint (安全点)

2.2 常见的safepoint场景

2.3 safepoint插入的位置

三 锁的分类

3.1 线程是不是需要锁住同步资源

3.1.2 悲观锁

3.1.2 乐观锁

3.2 多个线程竞争锁的是否需要排队

3.2.1 公平锁

3.2.2 非公平锁

3.3. 一个线程是否可以多次获取同一个锁

3.3.1 可重入锁

3.3.2 不可重入锁

3.4 多个线程能不能共享一把锁

3.4.1 共享锁

3.4.2 排它锁、互斥锁

3.5 根据获取锁失败后是否阻塞分类

3.5.1 自旋锁

3.6 根据线程数量和竞争状态激烈程度分类

3.6.1 重量级锁

3.6.2 轻量级锁

3.6.3 偏向锁

四 什么是Lock Record?

五 什么是监视器或者Monitor

5.1 监视器

5.2 工作流程

5.2.1 线程竞争锁

5.2.2 线程锁竞争成功

5.2.3 线程竞争锁失败

5.2.4 运行中的线程调用wait方法后,会被挂起

5.2.5 阻塞队列中线程时间到期或者被唤醒

5.2.6 轻量级锁膨胀之后,会导致竞争的线程加入到竞争失败队列_cxq中,然后挂起线程


一 对象在内存中的布局? 创建一个Object对象占用多少内存?

1.1 对象在内存中的布局?

我们知道,对象是放在堆中的,那么对象是怎么构成的?只包括实例数据吗?堆中的对象主要由三部分组成:对象头(object header)、实例数据(instance data)和对齐填充(padding)。

对象头:主要由三部分组成,包括mark word(关于hashcode、锁或者GC分代信息的记录), class pointer(指向方法区class的指针)、数组长度。

其中mark word的长度,根据是32位操作系统还是64位操作系统不同而不同,如果是32位操作系统,那么长度是32位;如果是64位操作系统,那么长度是64位。以下几点需需要我们注意:

第一:对象刚刚创建的时候,没有线程竞争它,也就是没有遇到同步代码块或者同步方法等。此时处于无锁状态,但是并不会立即生成hashcode数据,而是被调用hashcode方法的时候才会生成。

第二:在偏向锁加锁的时候,如果该线程调用了hashcode,那么此时因为偏向锁没有保存hashcode,所以偏向锁会升级为轻量级锁。

第三:轻量级锁会把锁对象的mark word拷贝到自己线程栈帧中的lock record,在轻量级锁释放的时候displaced mark word又被替换回mark word。所以轻量级锁hashcode、分代信息等都是保存在持有锁的线程的栈帧中的displaced mark word

第四:对于重量级锁,或者轻量级锁释放失败需要升级为重量级锁的时候,hashcode、分代信息是保存在监视器ObjectMonitor中的_header字段中

 

mark word结构如图所示:

 

实例数据:就是实例中根的字段和数据,如果没有数据,那么实例的长度为0,比如Object对象。

 

对齐填充:因为Java对象必须是8字节的倍数,如果不是8的倍数则对齐填充

 

1.2 创建一个Object对象占用多少内存?

原理: object header size + instance data size + padding

object header size = mark word size(8) + class pointer(4) + array size(如果有数组才有这个,object这里为0) = 12

instance data size = 0 (object没有字段)

padding = 4 (java对象必须是8的倍数,现在12不是8的倍数,所以需要对齐填充,比12大的最小8的倍数就是16,所以需要4字节对齐填充)

所以:创建一个Object对象占用多少内存总共需要16字节内存

 

二Safepoint(安全点)?

在JVM中,有时候需要执行一些全局性的操作,但是并不是在字节码执行的任何时候都适合去做这些操作,如果这样有可能导致异常。比如回收垃圾,需要暂停所有应用线程,但是只有等所有的应用线程暂停才可以开始。

2.1 什么是safepoint (安全点)

safepoint就是一个安全点,所有的线程执行到安全点的时候就会去检查是否需要执行safepoint操作,如果需要执行,那么所有的线程都将会等待,直到所有的线程进入safepoint。然后在JVM执行之后,所有的线程再恢复执行。

 

2.2 常见的safepoint场景

第一:GC回收

第二:偏向锁撤销

 

2.3 safepoint插入的位置

JVM会在字节码执行的某些时刻才会去做这些事情,常见的Safepoint位置有循环的结尾、方法返回之前、抛出异常的位置等。之所以选择这些位置作为safepoint的插入点,主要的考虑是避免程序长时间运行而不进入safepoint。比如GC的时候必须要等到Java线程都进入到safepoint的时候VMThread才能开始执行GC,如果程序长时间运行而没有进入safepoint,那么GC也无法开始,JVM可能进入到Freezen假死状态。

 

三 锁的分类

3.1 线程是不是需要锁住同步资源

3.1.2 悲观锁

认为每次都有其他线程修改数据,可能带来并发问题,所以在读写数据的时候都会对资源加锁,其他线程只能等待当前线程释放锁之后再抢占锁。常见的实现比如数据库Java中synchronized关键字或者MySQL排它锁。

 

场景:适用于写多的场景。因为使用乐观锁会不停的自旋,如果写线程多的话会消耗大量的CPU资源,从而影响性能。

 

3.1.2 乐观锁

不会或者只有很少的线程可能会修改数据,为资源提供提供一个版本号,每次比较版本更新前后是否一致,如果一致,则修改成功;否则重新获取新版本数据,重新进行修改,直到修改成功。常见的实现就是CAS。

 

场景:适用于读多写少的场景。因为写少,所以并发量就少,那么即使不停的自旋,问题也不大,而且也避免了加锁这种重量级的操作。

 

3.2 多个线程竞争锁的是否需要排队

3.2.1 公平锁

线程进入队列排队,根据先进先出的顺序获取锁

3.2.2 非公平锁

没有排队的锁

 

3.3. 一个线程是否可以多次获取同一个锁

3.3.1 可重入锁

可以多次获取同一个锁

 

3.3.2 不可重入锁

不能多次获取同一个锁

 

3.4 多个线程能不能共享一把锁

3.4.1 共享锁

多个线程可以共享一把锁

 

3.4.2 排它锁、互斥锁

线程之间不能共享一把锁

 

3.5 根据获取锁失败后是否阻塞分类

3.5.1 自旋锁

如果线程获取锁之后,还可以不停的重试,进行自旋,直到成功或者超过自旋次数。

 

3.6 根据线程数量和竞争状态激烈程度分类

3.6.1 重量级锁

执行同步代码块的时候或者执行同步方法的时候,需要为锁对象创建监视器,监视器用于关联锁对象、以及锁对象的原始头信息、重入次数、竞争失败队列、竞争队列和阻塞队列等信息,所以保存的东西占用的内存量很多,尤其是线程并发量大的时候;另外线程竞争失败的队列需要进入队列挂起或者线程阻塞也需要挂起,当挂起的时间到期又需要唤醒线程,等待和唤醒是属于系统调用,会涉及到CPU在用户态和内核态切换,线程太多就会频繁切换。

 

 

3.6.2 轻量级锁

当只有少量线程的时候,并发程度很低,不存在同时竞争锁的情况,只需要尝试将自己的线程栈帧中的锁记录信息设置到锁对象中就可以,不需要进行等待和唤醒等操作,所以不存在内核态和用户态的切换,只有系统调用才会发生内核态和用户态的切换;而且还少了操作系统调度,提升了性能。所以它是轻量级的。

 

3.6.3 偏向锁

当经常只有一个线程执行同步代码块的时候,其实复制mark word到锁记录displaced mark word都是没有必要的, 只要在锁对象中设置一个线程标识,每一次校验线程ID是不是持有锁的线程ID, 如果持有则可以执行同步代码块,没有多余的其他操作。这个偏向于某一线程的锁,就叫做偏向锁。

 

四 什么是Lock Record?

当锁对象升级到轻量级锁的时候,线程运行的时候在自己栈帧中分配的一块空间,用于存储锁对象头中mark word原始信息,拷贝过来的mark word也叫作displaced mark word,这块空间就是Lock Record,用于保存锁记录信息。

为什么要将锁对象的mark word拷贝到持有轻量级锁的线程的栈帧中?这样获取锁之后,还能保存锁对象的原始的hashcode、分代信息等。这也是轻量级锁和偏向锁的区别之一,否则如果只是和偏向锁类似,将锁对象通过一个线程ID标识当前被哪一个线程持有,那么在释放锁的时候只需要将这个线程ID清空就可以。但是如何保存锁对象的原始信息,比如hashcode和分代信息等,那就只有升级为重量级锁了,由监视器去保存这些东西。在代码实现锁记录主要由BasicObjectLock和BasicLock实现,一个BasicLock表示保存锁记录;BasicObjectLock关联锁对象和BasicLock。
 

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {

  friend class VMStructs;

 private:

  BasicLock _lock; // 锁记录

  oop       _obj;  // 关联的锁对象              

}



class BasicLock VALUE_OBJ_CLASS_SPEC {

  friend class VMStructs;

 private:

  volatile markOop _displaced_header; // 保存锁对象的mark word

}

 

五 什么是监视器或者Monitor

5.1 监视器

Monitor,又叫做ObjectMonitor或者监视器或者管程,主要用来监视锁对象。当轻量级锁升级到重量级锁的时候,此时JVM会创建一个管程或者说监视器来控制对共享资源的访问,保证多个线程访问同一个资源的时候可以达到互斥和同步的效果。

监视器主要包含以下字段信息:

ObjectMonitor() {

    _header       = NULL; // 锁对象mark word信息,保存hashcode或者分代信息

    _count        = 0; // 记录线程获取锁的次数

    _waiters      = 0, // 等待获取锁的线程

    _recursions   = 0; // 记录锁的重入次数

    _object       = NULL; // 锁对象

    _owner        = NULL;  // 指向持有这个monitor对象(锁)的线程

    _WaitSet      = NULL;  // 存放处于等待状态的线程

    _WaitSetLock  = 0 ;

    _Responsible  = NULL ;

    _succ         = NULL ; // 假定继承人,被唤醒的线程,准备获取锁

    _cxq          = NULL ; // 竞争失败队列

    FreeNext      = NULL ;

    _EntryList    = NULL ; // 存放竞争锁的线程队列

    _SpinFreq     = 0 ; // 自旋频率

    _SpinClock    = 0 ;

    OwnerIsThread = 0 ; // 锁的持有者是否是线程,如果是轻量级锁,持有者最开始可能是lock record,并不是线程

    _previous_owner_tid = 0;

  }

 

5.2 工作流程

 

5.2.1 线程竞争锁

新到来的线程释放锁唤醒的线程(假定继承人)会同时通过CAS方式竞争锁,即将监视器中的线程持有者_owner置为自己

 

5.2.2 线程锁竞争成功

线程竞争锁成功,就可以进入临界区执行代码

 

5.2.3 线程竞争锁失败

根据不同的唤醒策略QMode,那么失败后的处理结果不一样:

情况一:QMode= 2

失败的线程封装成ObjectWaiter,然后放入到_cxq锁竞争失败队列,并且唤醒队首的线程(此时唤醒的线程就是假定继承者),然后返回。

 

情况二:QMode = 3

失败的线程封装成ObjectWaiter,然后放入到_cxq锁竞争失败队列,并且将_cxq队列中所有元素插入到_EntryList队尾,并且唤醒_EntryList队首线程(此时唤醒的线程就是假定继承者)。

 

情况三:QMode = 4

失败的线程封装成ObjectWaiter,然后放入到_cxq锁竞争失败队列,并且将_cxq队列中所有元素插入到_EntryList队首,并且唤醒_EntryList队首线程(此时唤醒的线程就是假定继承者)。

 

5.2.4 运行中的线程调用wait方法后,会被挂起

运行中的线程调用wait方法后,会被挂起,放入到阻塞队列中_WaitSet中

 

5.2.5 阻塞队列中线程时间到期或者被唤醒

阻塞队列_WaitSet中线程时间到期或者被唤醒(notify),不会立刻抢占锁,而是先被放入到_EntryList中或者_cxq队列中(取决于Knob_MoveNotifyee这个值)

 

5.2.6 轻量级锁膨胀之后,会导致竞争的线程加入到竞争失败队列_cxq中,然后挂起线程

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

莫言静好、

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

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

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

打赏作者

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

抵扣说明:

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

余额充值