本片文章主要讲解synchronized的锁升级过程,以及volatile的两个特性的理解
1.线程阻塞的代价:
首先需要明白线程阻塞的代价,线程阻塞(挂起)的时候,需要操作系统的接入,在用户态和内核态之间进行切换,用户态切换到内核态的时候,会将线程中的变量和参数传给内核,内核需要记录在切换时的一些寄存器的值,变量。如果是高并发的情况下,且同步代码块执行时间很快的情况之下,这样是很浪费cpu的资源的,因为线程挂起的时间超过同步代码块执行的时间。
在jdk1.6之前,直接使用重量级锁(独占式悲观锁)再上诉的情况下无疑是浪费CPU性能的。
jdk1.6之后,对synchronized进行优化,在代码中使用synchronized,并不是直接需要惊动操作系统老大,而是存在一个锁升级的过程。
2.锁升级过程
本文所说的jvm都是hotspot
理解synchronized升级过程,首先需要在理解一个概念,对象的内存结构。
2.1对象的内存结构:
分三部分:对象头(markword,klass),instance data,padding。
关于对象内存结构:很简单的一个概念
三个部分我这里简单说明一下:
markword:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁信息等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit(4个字节,8个字节)。
klass:即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(我的理解看哪的Class,简称klass,小白一个,大牛见谅)。
实际数据部分:就是定义的各种类型的字段内容,以及父类中继承的。
padding:对齐区域,hotspot,要求对象必须是8字节的整数倍,(具体为啥这么要求不明白,可能就是为了方便加载),对齐区域的作用就是凑8整数倍。
2019年有一道大厂的面试题:new Object();对象占多少字节。
具体计算方式:
32位操作系统:对象头为8字节,markWord为4字节,klass为4字节
64位操作系统:
(未压缩指针)对象头为16字节,markword为8字节,klass为8字节
(压缩指针)对象头为12字节,markword为8字节,klass为4字节
new Object();对象内存多大,64为操作系统中,无论指针压缩与否都是16字节。
想要理解锁升级的过程,主要就是对锁资源对象的markword修改
markword8个字节,都放了什么,都代表什么,如图:
2.2偏向锁:
之所以称之为偏向锁,是因为偏向锁会偏向上一个获取偏向锁资源的线程,这样效率更高,如果一段时间内只有一个线程去获取这个锁资源的话,只有第一次的时候需要CAS操作,将偏向锁资源markword记录当前线程的ID,下一次这个线程还需要获得锁资源对象的时候,直接比较记录的指针是否是当前指针id即可,无需CAS操作,所以称之为偏向。
缺点:虽然偏向锁很高效,但是有一个问题,就是偏向锁升级的问题,如果存在高并发竞争,会导致偏向锁撤销以及升级的过程,这过程会在全局安全点上进行,会导致std,影响效率。
获取偏向锁资源的步骤:
1.访问锁资源中的markword,最后3个bit,依照上图,是否是101,可偏向的状态。
2.可偏向的话,查看markword中是否指向当前线程ID,如果指向直接获取锁资源。
3.如果不是的话,通过CAS操作,将当前线程ID“贴”到markword中,如果成功,获取锁资源,执行同步代码。
4.如果不成功,就说明存在竞争关系,存在竞争关系就会造成偏向锁升级为轻量级锁,这个过程会在全局安全点,造成std。
2.3轻量级锁(自旋锁,又称为无锁(不是001,两者不要混淆)),直接称之为CAS锁。
在对轻量级锁进行竞争的时候,jvm会在线程栈帧中创建一个LockRecord(所记录空间),其中记录了锁资源对象的markword,通过自旋的方式,将锁资源对象的markword修改为当前线程的LockRecord,成功了,可以获取锁资源,执行同步代码,如果不成功,就一直自选,超过一定次数之后,或者是竞争进一步加剧的时候,会继续升级为重量级锁。
jdk1.6版本之前,关于何时升级为重量级锁,默认线程的自选次数超过10次,或者是CAS的线程超过cpu核数的一半,这些个参数可以通过jvm进行配置,但是jdk1.6版本之后,jdk引入自适应自选,有jvm进行控制所升级的阀值条件。
2.4重量级锁:向os申请锁资源,CPU操作级别有ring3(用户态)变为ring0(内核态),线程进行阻塞状态,等待操作系统的调度。
锁升级过程:
上图是锁资源对象创建的时候,以及升级的路线图
大部分的路线,上面都已经说的差不多了,这里说明一下普通对象,和匿名偏向对象。
jdk1.6以上默认是开启偏向锁的,但是我们在本地测试的时候,通过JOL工具,new对象的时候,观察打印的markword字节情况,发现还是“001”,无锁状态,这个是因为hotspot启动的时候,内部也是有很的sync同步代码,启动的时候会有高竞争的情况,我们说过偏向锁并不适用于高并发的情况,所以是jvm的自身的优化。
偏向锁的开启会有延迟时间,默认是4秒。
如果你想要测试的话,可以设置启动jvm参数:-XX:BaisedLockingStartupDelay = 0
设置延迟时间为0,或者是线程睡4秒,之后再new对象,就会发现对象markword变为“101”。
3.synchronized,在hotspot底层实现
这个可以去哔哩哔哩上面查马士兵视频学习,里面有详细的跟踪步骤,我这里只是写一个结果,个人感觉记住即可
一条指令:lock comxchg
同时CAS,如何保证其原子性,可是通过这条指令:lock comxchg
4.锁消除
举例说明:StringBuffer是线程安全的,因为其内部的方法都加上synchronized修饰,
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
sb这个变量不会被共享,所以这个时候,jvm会自动消除锁。
5.锁粗化
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
如果不存在锁粗化的话,每次调用append方法就会加锁,解锁,循环100次就需要100次的加锁,解锁,jvm针对这种操作,将锁粗化,扩大锁的范围,字需要一次加锁和解锁即可。
本篇文章,主要介绍了synchronized的锁升级过程,以及jvm层的实现指令(记住即可,有印象即可),还有简单介绍锁粗化,锁消除这两个概念,就是一些面试的小点,平时开发用不到,稍微阅读以下即可。