锁的四种状态与锁升级过程

2025-05-29 0 27

锁的四种状态与锁升级过程

一、前言

的状态总共有四种,级别由低到高依次为:、偏向、轻量级重量级,这四种状态分别代表什么,为什么会有升级?其实在 JDK 1.6之前,synchronized 还是一个重量级,是一个效率比较低下的,但是在JDK 1.6后,Jvm为了提高的获取与释放效率对(synchronized )进行了优化,引入了 偏向 和 轻量级 ,从此以后的状态就有了四种(无、偏向、轻量级、重量级),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行升级(从低级别到高级别),不能降级(高级别到低级别),意味着偏向升级成轻量级后不能降级成偏向。这种升级却不能降级的策略,目的是为了提高获得和释放的效率。

二、的四种状态

在 synchronized 最初的实现方式是 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,这种方式就是 synchronized实现同步最初的方式,这也是当初开发者诟病的地方,这也是在JDK6以前 synchronized效率低下的原因,JDK6中为了减少获得和释放带来的性能消耗,引入了“偏向”和“轻量级”。

所以目前状态一种有四种,从级别由低到高依次是:无、偏向,轻量级,重量级状态只能升级,不能降级

如图所示:

锁的四种状态与锁升级过程

三、状态的思路以及特点

锁的四种状态与锁升级过程

四、对比

锁的四种状态与锁升级过程

五、Synchronized

synchronized 用的是存在Java对象头里的,那么什么是对象头呢?

5.1 Java 对象头

我们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:MarkWord(标记字段)和KlassPointer(类型指针)

Mark Word:默认存储对象的HashCode,分代年龄和标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

在上面中我们知道了, synchronized 用的是存在Java对象头里的,那么具体是存在对象头哪里呢?答案是:存在对象的对象头的Mark Word中,那么MarkWord在对象头中到底长什么样,它到底存储了什么呢?

在64位的虚拟机中:

锁的四种状态与锁升级过程

在32位的虚拟机中:

锁的四种状态与锁升级过程

下面我们以 32位虚拟机为例,来看一下其 Mark Word 的字节具体是如何分配的

:对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放对象分代年龄,1bit 用来存放是否偏向的标识位,2bit 用来存放标识位为01

偏向: 在偏向中划分更细,还是开辟 25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 Epoch,4bit 存放对象分代年龄,1bit 存放是否偏向标识, 0表示无,1表示偏向的标识位还是01

轻量级:在轻量级中直接开辟 30bit 的空间存放指向栈中记录的指针,2bit 存放的标志位,其标志位为00

重量级: 在重量级中和轻量级一样,30bit 的空间用来存放指向重量级的指针,2bit 存放的标识位,为11

GC标记: 开辟30bit 的内存空间却没有占用,2bit 空间存放标志位为11。

其中无和偏向标志位都是01,只是在前面的1bit区分了这是无状态还是偏向状态

关于内存的分配,我们可以在git中openJDK中 markOop.hpp 可以看出:

  1. public:
  2. //Constants
  3. enum{age_bits=4,
  4. lock_bits=2,
  5. biased_lock_bits=1,
  6. max_hash_bits=BitsPerWord-age_bits-lock_bits-biased_lock_bits,
  7. hash_bits=max_hash_bits>31?31:max_hash_bits,
  8. cms_bits=LP64_ONLY(1)NOT_LP64(0),
  9. epoch_bits=2
  10. };
  • age_bits: 就是我们说的分代回收的标识,占用4字节
  • lock_bits: 是的标志位,占用2个字节
  • biasedlockbits: 是是否偏向的标识,占用1个字节
  • maxhashbits: 是针对无计算的hashcode 占用字节数量,如果是32位虚拟机,就是 32 – 4 – 2 -1 = 25 byte,如果是64 位虚拟机,64 – 4 – 2 – 1 = 57 byte,但是会有 25 字节未使用,所以64位的 hashcode 占用 31 byte
  • hash_bits: 是针对 64 位虚拟机来说,如果最大字节数大于 31,则取31,否则取真实的字节数
  • cms_bits: 不是64位虚拟机就占用 0 byte,是64位就占用 1byte
  • epoch_bits: 就是 epoch 所占用的字节大小,2字节。

5.2 Monitor

Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的,称为内部或者 Monitor

Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该的线程的唯一标识,表示该被这个线程占用。

Synchronized是通过对象内部的一个叫做监视器(monitor)来实现的,监视器本质又是依赖于底层的操作系统的 Mutex Lock(互斥)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的我们称之为重量级

随着的竞争,可以从偏向升级到轻量级,再升级的重量级(但是的升级是单向的,也就是说只能从低到高升级,不会出现的降级)。JDK 1.6中默认是开启偏向和轻量级的,我们也可以通过-XX:-UseBiasedLocking=false来禁用偏向

六、的分类

6.2 无

是指没有对资源进行定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。

6.3 偏向

初次执行到synchronized代码块的时候,对象变成偏向(通过CAS修改对象头里的标志位),字面意思是“偏向于第一个获得它的线程”的。执行完同步代码块后,线程并不会主动释放偏向。当第二次到达同步代码块时,线程会判断此时持有的线程是否就是自己(持有的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放,这里也就不需要重新加。如果自始至终使用的线程只有一个,很明显偏向几乎没有额外开销,性能极高。

偏向是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得,从而降低获取带来的消耗,即提高性能。

当一个线程访问同步代码块并获取时,会在 Mark Word 里存储偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加和解,而是检测 Mark Word 里是否存储着指向当前线程的偏向。轻量级的获取及释放依赖多次 CAS 原子指令,而偏向只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。

偏向只有遇到其他线程尝试竞争偏向时,持有偏向的线程才会释放,线程是不会主动释放偏向的。

关于偏向的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向的线程,然后判断对象是否处于被定状态。如果线程不处于活动状态,则将对象头设置成无状态,并撤销偏向,恢复到无(标志位为01)或轻量级(标志位为00)的状态。

6.4 轻量级(自旋)

锁的四种状态与锁升级过程

轻量级是指当是偏向的时候,却被另外的线程所访问,此时偏向就会升级为轻量级,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取,线程不会阻塞,从而提高性能。

轻量级的获取主要由两种情况:① 当关闭偏向功能时;② 由于多个线程竞争偏向导致偏向升级为轻量级

一旦有第二个线程加入竞争,偏向就升级为轻量级(自旋)。这里要明确一下什么是竞争:如果多个线程轮流获取一个,但是每次获取的时候都很顺利,没有发生阻塞,那么就不存在竞争。只有当某线程尝试获取的时候,发现该已经被占用,只能等待其释放,这才发生了竞争。

在轻量级状态下继续竞争,没有抢到的线程将自旋,即不停地循环判断是否能够被成功获取。获取的操作,其实就是通过CAS修改对象头里的标志位。先比较当前标志位是否为“释放”,如果是则将其设置为“定”,比较并设置是原子性发生的。这就算抢到了,然后线程将当前的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个,但是没有发生竞争,或者发生了很轻微的竞争,那么synchronized就用轻量级,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

6.4 重量级

重量级显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果竞争情况严重,某个达到最大自旋次数的线程,会将轻量级升级为重量级(依然是CAS修改标志位,但不修改持有的线程ID)。当后续线程尝试获取时,发现被占用的是重量级,则直接将自己挂起(而不是忙等),等待将来被唤醒。

重量级是指当有一个线程获取之后,其余所有等待获取该的线程都会处于阻塞状态。

简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资

五、总结

文中讲述了的四种状态以及是如何一步一步升级的过程,

原文地址:https://mp.weixin.qq.com/s/Lnx1j4njDymcxkAK2ZbwAg

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 锁的四种状态与锁升级过程 https://www.kuaiidc.com/112574.html

相关文章

发表评论
暂无评论