【全网最细系列】synchronized锁详解,偏向锁与锁膨胀全流程

简介: 【全网最细系列】synchronized锁详解,偏向锁与锁膨胀全流程

前言

从实战出发,只讲干货,这里是战斧的专栏。这一期我们还是一篇硬核内容,详细剖析Synchronized的原理。鉴于这样的硬核程度,我真诚建议读者需要具备一些synchronized的基础知识,当然,作为比较,也会稍微讲讲juc里的Lock,但Lock会有一篇专门的内容来讲述,这里只会简单提一下。


synchronized 与 Lock的异同

关于Synchronized与lock的问题,有相当多人都讲过,我也不卖关子,直接讲他们最本质的异同:模型相同、层级不同


模型相同

这里说的模型特指synchronized 膨胀到重量级锁之后的模型。这时,他和lock锁的模型是一致的,都是基于对象,都包含同步区域、阻塞队列、等待队列。当然,也都具备阻塞和唤醒线程的能力。


层级不同

synchronized 是jdk提供的关键字,其功能实现是c++完成的。而lock则是juc包内的,其功能是java语言实现的。应当注意的是,两者的几乎所有不同,都是因为层级不同导致的,例如以下方面


  1. 使用方法不同
  2. synchronized作为关键字直接修饰方法,或者在方法体中使用;而lock则需要手动申明出对象,然后在方法中使用
  3. synchronized编译后自带异常处理,在线程出现异常时,会自动释放锁;而lock则需要在代码中手动捕获和释放异常
  4. synchronized无法响应中断;而lock里常用的await方法则可以响应中断

  5. 扩展能力不同
  6. synchronized的用法固定和功能固定,只对外暴露一些jvm参数(如-XX:BiasedLockingBulkRebiasThreshold ),我们通常没法去改造他。而Lock则是java的一个接口,juc也提供了大量的类,我们完全可以根据需要使用和继承,来编写特定功能的锁,最终完成我们的功能,这一点相当重要,Lock锁之所以枝繁叶茂,各种功能齐全,难以一言蔽之,皆是因为它强大的扩展能力

  7. 功能范围不同
  8. 由于Lock强大的扩展性(主要是AQS的扩展性),导致synchronized和Lock在功能范围上根本不在一个级别
  9. synchronized是公平的-可重入的-独占锁。而Lock可以设定是否公平,是独占还是共享还是读写,甚至是否重入亦可设定。以至于在做功能对比时,通常synchronized 只能和 Lock下的ReentrantLock 做对比,而即使是ReentrantLock ,也比synchronized 功能更强,比如ReentrantLock可以允许多个条件队列,而synchronized只有一条。


了解synchronized

锁的种类

synchronized并不是单纯的一把锁,它其实分为两种:栈上锁阶段 及重量级锁阶段 。


栈上锁阶段:主要是在栈上做CAS,利用对象头和LockRecord(锁记录)来判断竞争锁的情况。该阶段又可以分为多个状态,如两种起始态:“无锁状态” 和 “匿名偏向状态”,进阶状态:“”偏向状态“ 和 “轻量锁状态”。


重量级锁阶段:基于objectMonitor对象的对象锁,只有该阶段才会出现线程的阻塞,才会出现阻塞与等待队列。我们在Object类里看到的wait() notify() 方法,其实是objectMonitor对象的方法。所以当你使用了wait() 或 notify() 后,必定会创建并进入重量级锁,这也是为什么这两个方法必须要放在synchronized代码块里的原因


锁的起始状态

上文已经说到,synchronized是有两个起始状态的:“无锁状态” 和 “匿名偏向状态”,我们先来看一张图:

d6d480dc3ac24e069fb89bf7ad0dee5c.png

这是一张64位虚拟机的对象头说明,我们可以看到有四种锁的状态, 有多少人是认为无锁状态升级后变成偏向锁状态的?其实无锁状态并不会变成偏向锁,无锁状态升级后直接就变成轻量级锁了。


在这里我们必须说明,偏向锁是jdk9引入的,并下放到jdk8u,换句话说偏向锁并不是一开始就有的。引入偏向锁的同时,一并提供了一个jvm参数-XX:+UseBiasedLocking,这个参数的意思就是开启偏向锁,当然你也可以选择关闭,只是jdk8u默认是开启的。是否开启偏向锁,将导致我们在创建对象时,对象头走向两种路线:


  • 不开启偏向锁 : 直接创建无锁状态的对象头
  • 开启偏向锁 : 直接创建匿名偏向状态的对象头,所谓匿名偏向,就是其他bit位和偏向锁一样,但threadID位置为空

锁的简单变化

看完了起始状态,我们再来看后续变化,简单的讲,其实就是就两条线 ,对应着两个起始状态。


  • 无锁 > 轻量级锁 > 重量级锁
  • 匿名偏向 > 偏向锁 > 轻量级锁 > 重量级锁

可以看到,只有初始状态为匿名偏向的,才会进入到偏向锁。如果一开始创建的对象头是无锁状态,遇到竞争后会直接进入轻量级锁。当然你或许还有疑问,比如当前是偏向锁,这时候我算一个hashcode,那hashcode又该存到哪里去呢?再比如偏向锁是怎么升级到轻量锁的?别急,我用一张图告诉你

1f1f82db817b497bb519b5a6a8d8bd9c.png


了解各个锁的特点

无锁与匿名偏向

我们都知道锁的状态基于对象头。因此对象在被创建的时候,对象头肯定会有一个初始默认状态,注意此时尚未有任何线程来获取锁。

而这个初始状态包含 无锁状态、匿名偏向状态 两种可能。在jdk8u引入偏向锁后,默认为我们开启了“使用偏向锁”,因此在jdk8u以后,对象的创建初始都是匿名偏向状态。当然jvm也支持设置一个延迟时间,在JVM启动初期的一定时间内,为避免偏向撤销的损耗,直接以无锁状态作为初始状态。


偏向锁

首先,我们需要知道,偏向锁并不是一开始就有的。而是jdk9中针对synchronized的优化,并同步到了jdk8u中。之所以叫偏向锁,顾名思义是指它偏心,始终偏向第一个获得锁的线程,并把这个线程存储在对象头里,之后只要是该线程来获取锁,都能快速批准。偏向锁利用CAS来保证判断和竞争操作。因此这个阶段的效率是很高的

2d729beb524d4a7bb49952ad7cbd5cfb.png


偏向锁需要讲的其实就两点:


1. 偏向锁适用于锁只有同一个线程来获取的场景

2. 偏向锁本质是一个标记,正因如此,即使获得偏向锁的线程退出了同步代码块,该标记也不会消失。当该线程下一次进入同步代码块时,仅判断一下对象头里的标记是不是本线程即可,也就是说仅一次CAS判断即可执行同步代码块,消耗极低.


轻量级锁

轻量级锁的存在其实很尴尬,在偏向锁没出现之前,它承担着重要功能,即以CAS的形式进行锁的判断和竞争,从而避免synchronized消耗太大。然而,在有了偏向锁后,它原本的功能很大程度上被偏向锁抢走了。

它的现状是,只有在多个线程轮流去执行同步代码块时才用的上。如果只有一个线程反复执行,偏向锁就够了。如果有多个线程同时竞争,则又只能靠重量级锁来解决。

1de23ba2a9784ecf9c157b515fc22f65.png


轻量级锁的情况就是:线程开辟一块LockRecord空间,然后把锁对象头的MarkWord复制一份到自己的LockRecord空间下,并且开辟一块owner空间留作执行锁使用。然后对锁对象头做CAS去存入本线程LockRecord的位置,比如线程A成功了,那就意味着A线程获得锁,线程A的owner也会存一份锁对象的内存地址,形成一个互相引用的形式。


轻量级锁需要澄清两点


1. 关于轻量级锁CAS失败会进入自旋状态的问题,网络上众说纷纭,我没有找到对应源码,所以认定不存在该步骤

2. 很多文章都会在提到轻量级锁时,线程开辟一块LockRecord空间。但其实只要有线程试图获取某个锁(进入某同步代码块),都会在栈帧中找到一个空闲的LockRecord空间,栈帧在创建时就包含一个监视器对象数组,长度会在编译期确定,要获取锁时,除非该锁已经在数组中了,否则会在该数组中找到一个空闲的空间,以它作为存储锁记录的空间,这是获取锁要执行的第一个方法,和锁的状态和竞争情况无关。


重量级锁

实际上如果真的有两个线程同时竞争锁,靠前面的锁是无法解决的,因为synchronized并不是共享锁,它一次只允许一个线程获得锁(进入同步代码块),而另一个线程则会被阻塞。

比如上图中,一个线程B在CAS轻量级锁失败时,意味着轻量级锁发生了竞争,此时竞争失败的线程会向JVM申请一个互斥量,并且将锁对象头的前30bit通过CAS指向申请到的互斥量地址(这次CAS和上次CAS不同,具体可看后文),然后自己进入睡眠状态。而当锁的持有线程A继续运行直到完成时,线程A想要释放锁资源的时候,发现原来的30bit位没有指向自己了,此时线程A就会释放锁,取唤醒前30bit位中互斥量对应的睡眠状态的线程B,线程B唤醒后尝试获取对象锁69f38a061db5426d9a434e423e38b79e.png


重量级锁需要注意几点:


1.重量级锁依赖于MonitorObject对象,每个java对象都有一个MonitorObject,所以每个java对象都可以作为同步锁

2. 重量级锁才会涉及线程内核态转换,做出阻塞和唤醒线程的动作,wait 和 notify方法其实是MonitorObject的方法

3. 重量级锁有两个队列,阻塞队列——执行到synchronized方法,但没获得锁,只能在方法外面排队,只有被唤醒并获得锁,才能进入方法体内部;等待队列——已经执行到synchronized方法内部,主动执行等待操作,暂时放弃锁,一旦被唤醒则并重新获得锁,则可以接着原来的代码执行

4. 等待队列的线程被唤醒后也会被扔到阻塞队列,和原本就在外面的阻塞线程同台竞争

9ac4118386ae48479ba48222fa55d0d1.png


锁升级流程图(c++源码级,全网唯一)

不多说废话,关于锁的升级,我直接祭出一张本人针对jdk8u c++源码,一个方法一个方法研究过后,写出的最细最全流程图。当然,我不敢说完全没有错漏,但这深刻度绝对少见。也因为是基于源码,所以我认为基本可以作为权威流程,对照其他文章加深理解。我图里也写了些总结,主要是纠正目前市面上一些文章的错误之处。

9f22842a3a4c4559a448a61a458cef6f.png


目录
相关文章
|
7月前
|
安全 算法 Java
可重入锁,不可重入锁,死锁的多种情况,以及产生的原因,如何解决,synchronized采用的锁策略(渣女圣经)自适应的底层,锁清除,锁粗化,CAS的部分应用
可重入锁,不可重入锁,死锁的多种情况,以及产生的原因,如何解决,synchronized采用的锁策略(渣女圣经)自适应的底层,锁清除,锁粗化,CAS的部分应用
|
5月前
|
Java 编译器 程序员
synchronized 原理(锁升级、锁消除和锁粗化)
synchronized 原理(锁升级、锁消除和锁粗化)
|
5月前
|
存储 安全 Java
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
37 0
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
|
10月前
|
存储 Java
一文打通锁升级(偏向锁,轻量级锁,重量级锁)
一文打通锁升级(偏向锁,轻量级锁,重量级锁)
|
11月前
|
存储 安全 Java
08.从源码揭秘偏向锁的升级
大家好,我是王有志。上一篇学习了synchronized的用法,今天我们深到synchronized的原理,来学习偏向锁升级到轻量级锁的过程。
121 0
08.从源码揭秘偏向锁的升级
|
存储 安全 Java
偏向锁 10 连问,被问懵圈了。。
偏向锁 10 连问,被问懵圈了。。
锁消除、锁粗化、锁升级区别与联系
锁消除、锁粗化、锁升级区别与联系
锁消除、锁粗化、锁升级区别与联系
|
SQL 运维 监控
锁优化|学习笔记
快速学习锁优化
76 0
|
存储
锁的粗化和细化
锁的粗化和细化
147 0
锁的粗化和细化