Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级

Synchronized关键字详解

    • synchronized的基本认识
    • 漫谈对象存储(仅针对于hotspot虚拟机)
    • 锁的存储
    • synchronized锁的升级
      • 偏向锁
      • 轻量级锁
      • 重量级锁
        • 重量级锁的进入与退出:

synchronized的基本认识

  • 数据不安全性的本质在于:共享数据存在并发操作
  • jdk1.6对synchronized进行了优化,引入了偏向锁和轻量级锁的概念。
  • synchronized有三种加锁的方式,不同的修饰类型,代表了锁的控制粒度
    • 修饰实例方法,作用于当前对象实例
    • 修饰静态方法,作用于当前类对象
    • 修饰代码块,作用于当前代码块,进入同步代码块前要获得给定对象的锁
  • HostSpot虚拟机中,对象的存储分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
  • 互斥不是根据修饰的是不是同一个类的方法判断,而是根据是不是同一个锁对象判断,是同一个锁对象才会互斥

漫谈对象存储(仅针对于hotspot虚拟机)

  • new创建一个对象的时候JVM层面实际上会创建一个instanceOopDesc对象,instanceOopDesc定义在instanceOop.hpp文件中,instanceOopDesc继承自oopDesc,oopDesc定义在Hotspot源码中的oop.hpp文件中
  • 普通实例对象中,oopDesc的定义包含两个成员,分别是_mark和_metadata
  • _mark表示对象标记,也就是MarkWord,记录了对象和锁的有关信息
  • _metadata表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针、_compressed_Klass表示压缩类指针
  • MarkWord的定义【32位虚拟机】:
    Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_第1张图片
  • MarkWord的存储情况【32位虚拟机】
    Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_第2张图片

锁的存储

  • 为什么任何对象都可以实现锁?
    • java中的每个对象都继承自Object类,而每个Object在JVM内部都有一个native的C++对象oop/oopDesc进行对应
    • 线程在获取锁的时候,实际上就是获得一个监视器对象,monitor可以认为是一个同步对象,所有的java对象是天生携带monitor。多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识。

synchronized锁的升级

在synchronized中,锁存在四种状态:无锁、偏向锁、轻量级锁、重量级锁。

偏向锁

  • 偏向锁的基本原理:当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁,而是直接比较对象头里面是否存储了当前线程的偏向锁。如果相等则表示偏向锁是偏向于当前线程的,就不再尝试获得锁了。
  • 偏向锁的获取:
    1. 获得锁对象的MarkWord,判断是否处于可偏向状态(biased_lock_bits=1,且ThreadId为空)
    2. 如果是可偏向状态,则通过CAS操作,把当前线程的ID写入MarkWord
      • 如果CAS成功,表示当前线程获得了偏向锁,并执行同步代码
      • 如果CAS失败,说明存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才执行
    3. 如果是已偏向状态(偏向锁不会主动释放锁),检查MarkWord中存储的ThreadID是否等于当前线程的ThreadID
      • 如果相等,直接执行同步代码
      • 如果不相等,就会进行偏向锁撤销(或重新偏向,或锁升级)
  • 偏向锁的撤销:偏向锁的撤销并不是把对象恢复到无锁可偏向状态,因为偏向锁并不存在锁释放的概念
    1. 原获得偏向锁的线程已经退出了临界区(同步代码块执行完),那么会将锁对象设置成无锁状态并重新偏向
    2. 如果未退出临界区,则暂停当前获得锁的线程,升级为轻量级锁。
  • 其他:偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;(在高并发场景下,有时开启偏向锁反而会提高获取锁资源的消耗)

轻量级锁

  • 升级轻量级锁的过程
    1. 线程在自己的栈帧中创建锁记录LockRecord
    2. 将锁对象的对象头中的MarkWord复制到刚刚创建的LockRecord中
    3. 将LockRecord中的Owner指针指向锁对象
    4. 将锁对象的对象头的MarkWord替换为 指向LockRecord的指针
      Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_第3张图片
  • 自旋锁:轻量级锁在加锁的过程中用到了自旋锁,当另外线程竞争锁时会自旋原地等待,而不是把线程阻塞。自旋也必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,反而会消耗过多CPU资源。默认情况下自旋次数为10次,可以通过-XX:PreBlockSpin=10来修改。
  • 自适应自旋锁:在jdk1.6之后引入了自适应自旋锁,这种自旋锁的自旋次数不是固定不变的。比如某个锁,自旋很少获得成功过,那么以后自旋次数会很少或者直接阻塞线程。
  • 轻量级锁的解锁:其实就是获得锁的逆向逻辑,通过CAS操作把线程栈帧中的LockRecord替换回到锁对象的MarkWord中,如果成功表示没有竞争,失败则会膨胀为重量级锁。

重量级锁

重量级锁的进入与退出:

当轻量级锁膨胀到重量级锁之后,就意味着线程只能被挂起阻塞来等待被唤醒了

  1. 编写测试文件:
    Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_第4张图片
  2. 执行javac命令,生成class文件
    在这里插入图片描述
  3. 执行javap -c命令,查看字节码
    Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_第5张图片
  4. 分析:
    • 每一个Java对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized修饰的同步代码块时,该线程得先获取synchronized修饰的对象对应的monitor
    • monitorenter表示去获得一个对象监视器,monitorexit表示释放monitor的所有权,使得其他被阻塞的线程可以去尝试获得这个监视器
    • monitor依赖操作系统的MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态和内核态之间来回切换,严重影响了锁的性能。
      重量级锁加锁的基本流程:
      Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_第6张图片

你可能感兴趣的:(Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级)