提升--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虚拟机的对象头包括两部分信息:

  1. markword
    第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。
  2. 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引入的一项多线程优化。

  1. 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
  2. 经过统计实际多数情况下,进入synchronized 代码块,其实只有一个线程.
  3. 如果在运行过程中,**遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁
  4. 将锁恢复到标准的轻量级锁

它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

偏向锁会记录,当前这个线程的指针到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 - 偏向锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁

锁机制的切换是根据竞争激烈程度进行的

  1. 在几乎无竞争的条件下, 会使用偏向锁
  2. 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁,
  3. 在重度竞争的情况下, 会升级到重量级锁。

锁升级的执行过程:

  1. 偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁
  2. 有争用 - 锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁
  3. 自旋超过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锁升级的主要内容,如果未能解决你的问题,请参考以下文章

死磕 java同步系列之synchronized解析

从三个层面解析synchronized原理

synchronized的锁优化是怎么处理的?

synchronized实现解析

synchronized实现解析

synchronized实现解析