提升--06---解析Synchronized锁升级
Posted 高高for 循环
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了提升--06---解析Synchronized锁升级相关的知识,希望对你有一定的参考价值。
Synchronized锁升级
多线程–10–偏向锁、轻量级锁、重量级锁、volatile
基础知识
1. CAS算法
- 即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
CAS详情 提升–05—并发编程之—原子性
2. 用户态与内核态
cpu分不同的指令级别
- linux内核跑在ring 0级, 用户程序跑在ring 3,对于系统的关键访问,需要经过kernel的同意,保证系统健壮性
- 内核执行的操作 - > 200多个系统调用 sendfile read write pthread fork
- JVM -> 站在OS老大的角度,就是个普通程序
普通程序想访问直接内存,网卡,显卡的内容,都得通过操作系统去访问
- 内核态:
执行在内核空间,能访问所有的指令 - 用户态:
只能访问用户能够访问的指令
JDK早期,synchronized 叫做重量级锁, 因为申请锁资源必须通过kernel, 系统调用
汇编指令
3. 对象结构
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:
- 对象头(Header)、
- 实例数据(Instance Data)
- 对齐填充(Padding)。
下图是普通对象实例与数组对象实例的数据结构:
对象头
HotSpot虚拟机的对象头包括两部分信息:
- markword
第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。 - klass
对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
数组长度(只有数组对象有)
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.
markword
- markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:
32位虚拟机在不同状态下markword结构如下图所示:
64位虚拟机在不同状态下markword结构如下图所示:
锁信息被记录在 markword里面
无锁态
偏向锁
轻量锁
重量锁
查看对象结构工具:JOL = Java Object Layout
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
import org.openjdk.jol.info.ClassLayout;
public class T01_Sync1 {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
锁的介绍
JDK早期,synchronized 叫做重量级锁, 因为申请锁资源必须通过kernel, 系统调用
1.偏向锁
Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。
- 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
- 经过统计实际多数情况下,进入synchronized 代码块,其实只有一个线程.
- 如果在运行过程中,**遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,
- 将锁恢复到标准的轻量级锁。
它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
偏向锁会记录,当前这个线程的指针到markword
偏向锁与轻量级锁理念上的区别:
轻量级锁:
- 在无竞争的情况下使用CAS操作去消除同步使用的互斥量
偏向锁:
- 在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了
意义:锁偏向于第一个获得它的线程。如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
2.轻量锁 自旋锁:
3.重量锁
- jvm向操作系统申请锁资源,需要进行用户态到内核态的切换.
- 完全依靠操作系统内部的互斥锁实现
Synchronized 原理
monitorenter 和 monitorexit :
Synchronized 是 由 JVM 实 现 的 一 种 实 现 互 斥 同 步 的 一 种 方 式 , 如 果 你 查 看 被 Synchronized 修 饰 过 的 程 序 块 编 译 后 的 字 节 码 , 会 发 现 , 被 Synchronized 修 饰 过 的 程 序 块 , 在 编 译 前 后 被 编 译 器 生 成 了monitorenter 和 monitorexit 两 个 字 节 码 指 令。
- monitorenter
- monitorexit
两 个 指 令 是 什 么 意 思 呢 ?
在 虚 拟 机 执 行 到 monitorenter 指 令 时 , 首 先 要 尝 试 获 取 对 象 的 锁 :
如 果 这 个 对 象 没 有 锁 定 , 或 者 当 前 线 程 已 经 拥 有 了 这 个 对 象 的 锁 , 把 锁 的 计 数 器 +1; 当 执 行 monitorexit 指 令 时 将 锁 计 数 器 -1; 当 计 数 器 为 0 时 , 锁 就 被 释 放 了 。
如 果 获 取 对 象 失 败 了 , 那 当 前 线 程 就 要 阻 塞 等 待 , 直 到 对 象 锁 被 另 外 一 个 线 程 释 放 为 止 。 Java 中 Synchronize 通 过 在 对 象 头 设 置 标 记 , 达 到 了 获 取 锁 和 释 放 锁 的 目 的 。
synchronized的横切面详解
java源码层级:
- synchronized(o)
字节码层级
- monitorenter moniterexit
JVM层级(Hotspot)
- InterpreterRuntime:: monitorenter方法
- synchronizer.cpp
- revoke_and_rebias
锁升级过程
new - 偏向锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁
锁机制的切换是根据竞争激烈程度进行的
- 在几乎无竞争的条件下, 会使用偏向锁
- 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁,
- 在重度竞争的情况下, 会升级到重量级锁。
锁升级的执行过程:
- 偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁
- 有争用 - 锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁
- 自旋超过10次,JDK1.6之后由自适应自旋 去做判断,升级为重量级锁 - ,进入等待队列(不消耗CPU)-XX:PreBlockSpin,直接阻塞线程,避免浪费处理器资源。
锁升级细节:
- new - 偏向锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁
- synchronized优化的过程和markword息息相关
- 用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位
锁的其他常见问题?
1.锁重入
- sychronized是可重入锁
- 重入次数必须记录,因为要解锁几次必须得对应
- 偏向锁 自旋锁 -> 线程栈 -> LR + 1
- 重量级锁 -> ? ObjectMonitor字段上
2.自旋锁什么时候升级为重量级锁?
1.6之前:
- 竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin,
- 或者自旋线程数超过CPU核数的一半,
1.6之后:
- 加入自适应自旋 Adapative Self Spinning , JVM自己控制
自适应自旋 :
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
- 如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
3. 为什么有自旋锁还需要重量级锁?
自旋锁:
- 自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗
- CPU在多个线程间切换,调度也是需要耗费资源的.
重量锁:
- 重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源
4. 偏向锁是否一定比自旋锁效率高?
- 不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁
- JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开
5. 偏向锁启动:
- 默认情况 偏向锁有个时延,默认是4秒
- why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
-XX:BiasedLockingStartupDelay=0
正常启动,JVM偏向锁有个时延,默认是4秒
匿名偏向锁
线程休眠5秒,再启动,new出来的对象,默认就是一个可偏向匿名对象101
线程休眠5秒,再启动,且对象有被Synchronized当做锁
偏向锁未启动,则普通对象如果被上锁,直接变成轻量锁
以上是关于提升--06---解析Synchronized锁升级的主要内容,如果未能解决你的问题,请参考以下文章