synchronized锁升级的过程
之前只是了解过一些悲观锁的底层原理,和他具体是如何锁住线程的一些细节,正好今天休息,结合一些文章和自己的实践操作,整理成了一篇关于synchronized锁升级的过程,希望能对大家有所帮助.
大佬:
Java 并发之 ReentrantLock 深入分析(与Synchronized区别) - 简书 (jianshu.com)
在最开始的时候,synchronized就是一个重量级的锁,效率不高,后来JDK对synchronized做了一些的优化,在上锁的时候会有一个锁升级的流程.
咱先来说一下重量级的锁吧.
重量级锁
也称为互斥锁和悲观锁,在JDK1.0-JDK1.2的版本变迁当中,重量级锁是那时候使用的锁,之前看到一些博客上写过重量级锁的本质其实是由JVM操控分配的,但是实际上JVM并不处理这些事,它是比较懒的,如果有线程向JVM讨要锁,JVM会直接去找它的老大哥,也就是操作系统来帮助他进行锁的分类.
为什么说重量级锁开销大呢?
主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
轻量级锁
在之后的版本变迁当中,出现了轻量级锁这个概念,它的实现底层是CAS乐观锁
CAS锁
下面让我们一起回顾一下CAS锁吧~
乐观锁:俗称CAS,英文是compare and swap 或者是 compare and exchange.
中文翻译就是比较再次交换,这和它的实现原理其实是相同的,我们可以一起内存图,比如在下图中,
我们可以看到有ABC三个线程,打个比方A线程先抢到了CPU的执行权,那么他会先读取到内存当中为100的值,放到自己的缓存当中,然后在暂存区中进行一个100 + 1的操作,表面我下一次去内存当中,会对这个值进行一个加1的操作,如果这时候,在我们还没对内存中的值进行改变之前,我们的线程执行权被B线程抢到了,这时候B线程会重复上面的步骤(它也同样进行了一个+1的操作),然后将暂存区中的101给赋值到内存当中,注意此时内存当中的值为101哦!然后这时候,当B线程执行完毕之后,线程执行权被A抢回来了,A在修改前会先读取一下内存当中的值,是否等于100,如果不等于100的话,那么他会执行回旋操作,也就是再次读取一下值,分别放入暂存区和缓存当中(注意这两个值是不一样的),缓存中的是修改后的值,然后再次读取一下内存当中的值,如果一样则执行保存操作,如果不一样则再次回旋,这也被称为一个原子性操作.
ABA问题
关于CAS其中有一个比较著名的问题,也被叫做ABA问题,
ABA问题指的是在线程ABC当中,A读取到了内存中的初始为100的值,然后这时候A去进行修改的时候,线程执行权被B抢走了,B将值修改为了101,这时候执行权被C抢走,C又修改为了100,这时候当A重新拿到执行权的时候,他会发现值是正常的,然后再次执行自己的修改操作,但是实际上我们知道,内存中的值早已经发生了改变,但是程序不知道呀! 那么我们该如何让我们的Java代码也知道呢?这时候我们可以在数据库中添加一个类似于版本号的version字段,然后每次进行操作的时候就会+1,让程序根据版本号得知之前是否已经被修改,但是在实际开发场景中,我们还是要根据自己的具体业务才行呢,不一定所有的乐观锁都需要我们去处理它本身的ABA问题.
CAS扩展
你们或许以为CAS做的操作只有这些,但是实际上并没有这么简单呢,我们甚至可能刚刚开始我们的底层核心之旅.
还有一种隐藏的可能性,当我们在判断暂存区中的值是否和内存中的值相等之后,正在进行赋值的时候,为什么不会被其他cpu抢走执行权呢?
这个就涉及到了我们的AtomicInteger原子类,他可以把我们类似count++的操作变成一种原子性操作,那么他到底是怎么实现的呢,这次我们一起走到代码的最底层去看看,放心不会太难的,因为我早已准备好了截图.
这个native意思是代表我们已经走到底了,他会去调用c++的代码.
在这张图中可以看到标红的部位,这是我们c++当中的代码 意思是当我们是多核CPU的情况下,在你进行修改值的时候,我会锁住防止其他线程对这个值进行修改,可以理解成为我们的悲观锁,也就是只允许一个锁进行访问资源,其余全部锁死.`
而在更底层的硬件层面,也就是CPU总线级别的,图例为4线程
如果用最根本的一句话带过,轻量级锁本质上就是不经过OS的锁,他会自动完成一个锁的分配,在用户空间JVM直接解决问题.而重量级锁是需要经过OS,也就是操作系统才能进行一个锁的分配,他是需要继续向上级进行汇报的.
那么如果我们已经有轻量级锁,还需要重量级锁嘛,他们的使用场景是怎么样的?
需要,轻量级锁的分配规则是需要消耗CPU资源的,重量级锁是不需要消耗CPU资源的,它的底层是一个队列,当OS释放完锁,它会从队列里拉人出来,逐次对锁进行获取释放.
但是在线程很多的时候,不建议使用轻量级锁,可以理解为CPU在这个切换的过程当中,就已经几乎要把它所有的资源消耗殆尽了,我们可以使用重量级锁,把很多的线程放进OS队列当中,然后全部冷冻住,等待OS去进行拿取.
偏向锁
偏向锁的概念: 偏向锁不是一把锁,它比轻量级锁还轻,这把锁不需要抢,只要第一个线程过来,就会自动偏向它,做到首位的一个占用,其实就是把线程的id号放到markword当中,让代码进行识别,它其实更加类似一个状态,第一个线程过来默认偏向,然后不需要跳过JVM中关于锁的一大堆代码,提高我们的效率.
锁升级的过程
当出现有多个线程来竞争锁的话,此时会开启一种名为(自旋锁)的竞争机制,所有的线程都开始CAS自旋操作,开始争抢锁的归属,那么偏向锁就失效了,此时锁就会发生(锁膨胀),升级为轻量级锁,如果线程过多,CPU处于频繁切换线程的情况下,那么这时候就轮到我们的重量级锁登场了,它可以帮助我们直接将CPU的损耗降到最低,阻塞所有的线程,放入队列当中,等待操作系统层面的唤醒,但是这也是有缺点的,锁的分配都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长.
为什么要设计偏向锁这个概念呢?
在我们的代码开发过程当中,我们可以发现在我们的源码当中,比如HashTable,StringBuffer这些类里面都有synchronized这把锁,但是真正在实际生产运行过程中,他们百分之70到80的时间段内,其实都是只有一个线程在运行的,而我却还要开启这个锁,这就显得对资源非常的浪费,于是我们的java开发人员又对synchronized进行了适当的优化,这就是偏向锁的由来,当我实际上只有一个线程,没有多个线程发生争抢情况的时候,那么我就直接可以使用偏向锁,相当于把名字往上一帖告诉锁,这把锁是我一个人的,你不需要再次开启锁争抢机制了,在Java代码层面是一个标记的概念,底层会根据标记进行判断.
Java对象头MackWorld
这里会涉及到一道经典的面试题,在这里分享一下.
一个object占多少个字节?
回答:是16个字节
首先要从对象布局还是看起,如下图所示.
过程分析:
对象头: 8个字节.
类型指针:4个字节.
实例数据:默认空参为0个字节,如果有个int会占4个字节.
对齐:在64位的系统中8字节对其,可以理解为我们生活中的集装箱补全机制,自动把加起来的字节和变为8的倍数.
比如当前是 8 + 4 + 0 = 12 不为8整除.
对其这位就会自动变为4,帮我们最后的字节数变为8的倍数,也就是16.
那么我们该怎么确切的观察到里面的布局呢?
java代码如下
publicclasstest {
publicstaticvoidmain(String[] args) {
Objecto=newObject();
Strings=ClassLayout.parseInstance(o).toPrintable();
//解析一个对象转换成一个可打印的对象
System.out.println(s);
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
//解析一个对象转换成一个可打印的对象(加锁后的情况)
}
}
}
pom.xml
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency
讲了很多,可能很多人都不知道,为啥会跟这道面试题有关系呢?
确实有关系,是和这里面的markword有关系,上锁的过程,其实就是mackword发生了变化,mackword中记录了锁信息,hashcode信息和垃圾回收信息,我们可以根据mackword的数值去判断,当前对象正持有哪一把锁.
可以根据这张表对应上图红框圈出来的部分.
为什么一定要有延时呢,打开偏向锁会不会提高我们的效率,为什么 ?
在这里我们需要回顾偏向锁的前提条件,是当我们明确知道只有一把锁不会发生锁争抢事件的时候,这个偏向锁的效率才是最高的,如果是在多线程情况下的,我们继续设置偏向锁,这个效率其实是非常低下的,可以想象很多个线程都需要执行锁撤销的动作,然后在锁上写上自己的名字,循环往复,所以JVM默认就不会给你开偏向锁,会设置个4秒的延迟,当然这个延时我们也可以自己定义,这就涉及到JVM调优了,我们可以根据自己的服务器去定位最适合自己的参数.
各自使用场景总结
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
重量级锁:有实际竞争,且锁竞争时间长。