Java中Sychronized的锁升级


一、Java 对象结构

Java 对象的结构分为三部分,分别是对象头、对象体和对齐字节。synchronized 用的锁是存在Java对象头里的,那么什么是对象头呢?

我们以 Hotspot 虚拟机为例进行说明,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 Klass Pointer(类型指针)。

  • Mark Word:主要用于存储自身运行时数据
  • Class Pointer:是指针,指向方法区中该 class 的对象,JVM 通过此字段来判断当前对象是哪个类的实例

Mark Word 所代表的「运行时数据」主要用来表示当前 Java 对象的线程锁状态以及 GC 的标志。而线程锁状态分别就是无锁、偏向锁、轻量级锁、重量级锁。
在这里插入图片描述

二、锁状态

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

  2. 偏向锁
    无锁状态下线程 A 初次执行到synchronized代码块的时候,锁对象变成偏向锁,执行完同步代码块后,线程并不会主动释放偏向锁,后续线程 A 再次访问同步代码时,不需要做任何的 check,直接执行(偏向于第一个获得它的线程),这样降低了获取锁的代价,提升了效率。
    无锁、偏向锁的 lock 标志位是一样的,即都是 01,无锁、偏向锁是靠字段 biased_lock 来区分的,0 代表没有使用偏向锁,1 代表启用了偏向锁。

  3. 轻量级锁
    在升级成偏向锁的过程中, 一旦有第二个线程参与竞争,就会立即膨胀为轻量级锁。企图抢占的线程一开始会使用自旋的方式去尝试获取锁。如果循环几次,其他的线程释放了锁,就不需要进行用户态到内核态的切换。
    JDK 1.7 之前是普通自旋,会设定一个最大的自旋次数,默认是 10 次,超过这个阈值就停止自旋。JDK 1.7 之后,引入了适应性自旋,如果这次自旋获取到锁了,自旋的次数就会增加;这次自旋没拿到锁,自旋的次数就会减少。

  4. 重量级锁
    在升级成轻量级锁的过程中,试图抢占的线程自旋达到阈值,就会停止自旋,那么此时锁就会膨胀成重量级锁。当其膨胀成重量级锁后,其他竞争的线程进来就不会自旋了,而是直接阻塞等待,并且 Mark Word 中的内容会变成一个监视器(monitor)对象,用来统一管理排队的线程。

三、monitor 对象如何管理线程

每个java对象都可以关联一个monitor对象,如果使用synchronized来进行加重量级锁的话,会在Mark word中存放monitor地址,来与monitor对象相关联。
在这里插入图片描述

  • Owner:拥有当前 monitor 对象的线程,即持有锁的那个线程。
  • WaitSet:当 Owner 线程调用 wait() 方法被阻塞之后,会被放到这里。等待时间到期的时候唤醒,或者其他线程唤醒,会重新进入 EntryList 当中。
  • EntryList:所有竞争锁的线程都会先进入ContentionQueue 中,ContentionQueue 中有资格的线程会被移动到这里。

四、锁升级

线程 A 进入 synchronized 开始抢锁,JVM 会判断当前是否是偏向锁的状态,如果是就会根据 Mark Word 中存储的线程 ID 来判断,当前线程 A 是否就是持有偏向锁的线程。如果是,则忽略 check,线程 A 直接执行临界区内的代码。

但如果 Mark Word 里的线程不是线程 A,就会通过自旋尝试获取锁,如果获取到了,就将 Mark Word 中的线程 ID 改为自己的;如果竞争失败,就会立马撤销偏向锁,膨胀为轻量级锁。

后续的竞争线程都会通过自旋来尝试获取锁,如果自旋成功那么锁的状态仍然是轻量级锁。然而如果竞争失败,锁会膨胀为重量级锁,后续等待的竞争的线程都会被阻塞。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44153131/article/details/129800422