重量级锁、轻量级锁、偏向锁与三者之间的转换过程

前言介绍

synchronized
采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程想再获取这个【对象锁】时就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心上下文切换。

  • synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

  • 如果加锁线程add没有运行完时间片就用完了,锁并不会释放,sub会进入阻塞状态,当锁释放之后会唤醒阻塞状态线程
    synchronized基本使用
    在这里插入图片描述

实现原理

前提知识

由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。

1.对象头形式
JVM中对象头的方式有以下两种(以32位JVM为例):

1.1.普通对象

1.2.数组对象
在这里插入图片描述
2.对象头的组成
2.1.Mark Word
这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。
为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
在这里插入图片描述
其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。

biased_lock lock 状态
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。

64位下的标记字与32位的相似,不再赘述:
在这里插入图片描述
2.2.class pointer
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

每个Class的属性指针(即静态变量)
每个对象的属性指针(即对象变量)
普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

重量级锁

Monitor(监视器或管程)
内置锁在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。
每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取monitor。
当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。
当获取到monitor时对象的markword中指向monitor地址,同时用cas操作将monitor中owner放入当前获取锁的线程。
在这里插入图片描述
在这里插入图片描述

轻量级锁

没有竞争时使用轻量级锁,减少传统重量级锁使用操作系统互斥量带来的开销。
执行到锁代码时,会创建锁记录(LockRecord)(Jvm层面)对象,每个线程的栈帧都会包含一个锁记录的结构
在这里插入图片描述
让锁记录Object reference指向锁对象,
并尝试替换Object的markWord,将markWord的值存入锁记录
在这里插入图片描述
如果替换成功,对象投中存储了锁记录地址和状态 00,表示有该线程给对象加锁,这时图如下
在这里插入图片描述
如果cas失败,有两种情况
1.如果是其他线程已经持有了该object轻量级锁,这时表明有竞争,进入锁膨胀过程
2.如果是自己执行了synchronized锁重入,那么再加一套lockRecord作为重入的计数
在这里插入图片描述当退出synchronized代码块时:

  • 如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
  • 如果锁记录的值不为null,这时使用cas将markword的值恢复给对象头,成功则解锁成功,失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

偏向锁

为什么有偏向锁优化
大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,
只有第一次使用将线程ID设置到对象的MarkWorld头,之后发现这个线程ID是自己的就表示没有竞争,不用重新加锁,以后只要不发生竞争,这个对象就归该线程所有
在这里插入图片描述

自旋锁

首先,内核态与用户态的切换上不容易优化。但通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。

如果锁的粒度小,那么锁的持有时间比较短(尽管具体的持有时间无法得知,但可以认为,通常有一部分锁能满足上述性质)。那么,对于竞争这些锁的而言,因为锁阻塞造成线程切换的时间与锁持有的时间相当,减少线程阻塞造成的线程切换,能得到较大的性能提升。具体如下:

当前线程竞争锁失败时,打算阻塞自己
不直接阻塞自己,而是自旋(空等待,比如一个空的有限for循环)一会
在自旋的同时重新竞争锁
如果自旋结束前获得了锁,那么锁获取成功;否则,自旋结束后阻塞自己
如果在自旋的时间内,锁就被旧owner释放了,那么当前线程就不需要阻塞自己(也不需要在未来锁释放时恢复),减少了一次线程切换。

“锁的持有时间比较短”这一条件可以放宽。实际上,只要锁竞争的时间比较短(比如线程1快释放锁的时候,线程2才会来竞争锁),就能够提高自旋获得锁的概率。这通常发生在锁持有时间长,但竞争不激烈的场景中。

自适应自旋锁

自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
相反的,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。
自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。

批量重偏向

如果对象被多个线程访问且没有竞争,这时候偏向T1线程的锁仍有可能偏向T2, 重偏向会重置对象的threadId,当撤销偏向锁阈值超过20次之后,jvm会在给这些对象加锁时重新偏向至加锁线程。

批量撤销

当撤销偏向锁阈值超过40次之后,jvm会觉得自己确实偏向错了,于是整个类的所有对象都会变为不可偏向,新建的对象也不可偏向的

锁消除

如果不会锁,jit会进行锁优化,将不会发生锁的锁优化掉

锁的变化过程图解

在这里插入图片描述

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值