synchronized锁升级的过程

简介: 之前只是了解过一些悲观锁的底层原理,和他具体是如何锁住线程的一些细节,正好今天休息,结合一些文章和自己的实践操作,整理成了一篇关于synchronized锁升级的过程,希望能对大家有所帮助.

synchronized锁升级的过程

1.png

之前只是了解过一些悲观锁的底层原理,和他具体是如何锁住线程的一些细节,正好今天休息,结合一些文章和自己的实践操作,整理成了一篇关于synchronized锁升级的过程,希望能对大家有所帮助.

大佬:

Java 并发之 ReentrantLock 深入分析(与Synchronized区别) - 简书 (jianshu.com)

在最开始的时候,synchronized就是一个重量级的锁,效率不高,后来JDK对synchronized做了一些的优化,在上锁的时候会有一个锁升级的流程.

2.png

咱先来说一下重量级的锁吧.

重量级锁

也称为互斥锁和悲观锁,在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的话,那么他会执行回旋操作,也就是再次读取一下值,分别放入暂存区和缓存当中(注意这两个值是不一样的),缓存中的是修改后的值,然后再次读取一下内存当中的值,如果一样则执行保存操作,如果不一样则再次回旋,这也被称为一个原子性操作.

3.png

ABA问题

关于CAS其中有一个比较著名的问题,也被叫做ABA问题,

ABA问题指的是在线程ABC当中,A读取到了内存中的初始为100的值,然后这时候A去进行修改的时候,线程执行权被B抢走了,B将值修改为了101,这时候执行权被C抢走,C又修改为了100,这时候当A重新拿到执行权的时候,他会发现值是正常的,然后再次执行自己的修改操作,但是实际上我们知道,内存中的值早已经发生了改变,但是程序不知道呀! 那么我们该如何让我们的Java代码也知道呢?这时候我们可以在数据库中添加一个类似于版本号的version字段,然后每次进行操作的时候就会+1,让程序根据版本号得知之前是否已经被修改,但是在实际开发场景中,我们还是要根据自己的具体业务才行呢,不一定所有的乐观锁都需要我们去处理它本身的ABA问题.

4.png

CAS扩展

你们或许以为CAS做的操作只有这些,但是实际上并没有这么简单呢,我们甚至可能刚刚开始我们的底层核心之旅.

还有一种隐藏的可能性,当我们在判断暂存区中的值是否和内存中的值相等之后,正在进行赋值的时候,为什么不会被其他cpu抢走执行权呢?

这个就涉及到了我们的AtomicInteger原子类,他可以把我们类似count++的操作变成一种原子性操作,那么他到底是怎么实现的呢,这次我们一起走到代码的最底层去看看,放心不会太难的,因为我早已准备好了截图.

这个native意思是代表我们已经走到底了,他会去调用c++的代码.

5.png

在这张图中可以看到标红的部位,这是我们c++当中的代码 意思是当我们是多核CPU的情况下,在你进行修改值的时候,我会锁住防止其他线程对这个值进行修改,可以理解成为我们的悲观锁,也就是只允许一个锁进行访问资源,其余全部锁死.`

6.png

而在更底层的硬件层面,也就是CPU总线级别的,图例为4线程

7.png

如果用最根本的一句话带过,轻量级锁本质上就是不经过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.png

过程分析:

对象头: 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的数值去判断,当前对象正持有哪一把锁.

9.png

可以根据这张表对应上图红框圈出来的部分.

10.png

11.png

为什么一定要有延时呢,打开偏向锁会不会提高我们的效率,为什么 ?

在这里我们需要回顾偏向锁的前提条件,是当我们明确知道只有一把锁不会发生锁争抢事件的时候,这个偏向锁的效率才是最高的,如果是在多线程情况下的,我们继续设置偏向锁,这个效率其实是非常低下的,可以想象很多个线程都需要执行锁撤销的动作,然后在锁上写上自己的名字,循环往复,所以JVM默认就不会给你开偏向锁,会设置个4秒的延迟,当然这个延时我们也可以自己定义,这就涉及到JVM调优了,我们可以根据自己的服务器去定位最适合自己的参数.

各自使用场景总结

偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。

轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。

重量级锁:有实际竞争,且锁竞争时间长。

目录
相关文章
|
2月前
|
存储 缓存 安全
关于Synchronized锁升级,你该了解这些
关于Synchronized锁升级,你该了解这些
77 3
|
7月前
|
安全 算法 Java
可重入锁,不可重入锁,死锁的多种情况,以及产生的原因,如何解决,synchronized采用的锁策略(渣女圣经)自适应的底层,锁清除,锁粗化,CAS的部分应用
可重入锁,不可重入锁,死锁的多种情况,以及产生的原因,如何解决,synchronized采用的锁策略(渣女圣经)自适应的底层,锁清除,锁粗化,CAS的部分应用
|
3月前
|
存储 安全 Java
Synchronized锁工作原理
Synchronized锁工作原理
|
5月前
|
Java 编译器 程序员
synchronized 原理(锁升级、锁消除和锁粗化)
synchronized 原理(锁升级、锁消除和锁粗化)
|
5月前
|
存储 安全 Java
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
37 0
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
|
6月前
|
存储 Java
面试~Synchronized 与 锁升级
面试~Synchronized 与 锁升级
37 0
|
10月前
|
安全 Java
锁升级原理
锁升级是指在多线程环境下,当一个线程持有了低级别的锁(如偏向锁或轻量级锁)时,如果有其他线程也要获取这个锁,那么就需要将锁升级为重量级锁。这样可以保证在并发情况下,多个线程之间的互斥访问。
162 1
|
11月前
|
存储 安全 Java
08.从源码揭秘偏向锁的升级
大家好,我是王有志。上一篇学习了synchronized的用法,今天我们深到synchronized的原理,来学习偏向锁升级到轻量级锁的过程。
120 0
08.从源码揭秘偏向锁的升级
|
12月前
|
Java 编译器
synchronized关键字(作用 + 特点 + 锁升级 + 锁优化 + 与 volatile 对比)
1. synchronized 的作用 1)保证原子性 2)保证内存可见性 3)保证有序性 2. synchronized 特点 3. 锁升级的过程 1)偏向锁 2)轻量级锁 3)重量级锁 4. 锁的优化操作 1)锁消除 2)锁粗化 5. synchronized 使用示例 1)修饰普通方法:锁当前实例对象 2)修饰静态方法:锁当前类对象 3)修饰代码块:指定锁哪个对象 6. volatile 的作用 1)保证内存可见性 2)保证有序性 7. synchronized 和 volatile 的区别
77 0
synchronized关键字(作用 + 特点 + 锁升级 + 锁优化 + 与 volatile 对比)
|
存储 Java
synchronized锁升级原理
synchronized锁升级原理
241 1
synchronized锁升级原理