【JUC进阶】详解synchronized锁升级

1. synchronized概述

synchronized是一个悲观锁,可以实现线程同步,在多线程的环境下,需在操作同步资源的时候先加锁,避免共享资源出现问题。

因为加锁可以使得一个线程在一个时间点内只有一个线程可以访问,这样增加了安全性。

但是这样却损失了程序的执行性能,因为在加锁、抢夺锁、释放锁需要从用户态切换成内核态,属于操作系统层面的,因此比较消耗性能。

于是,在JDK6之后便引入了“偏向锁”和“轻量级锁”,共有4种锁状态,级别由低到高依次为:无锁状态偏向锁状态轻量级锁状态重量级锁状态。这几个状态会随着竞争情况逐渐升级。

锁状态说明及升级图示

synchronized 可以用在实例方法、静态方法、代码块上

  1. 修饰实例方法,对当前实例对象this加锁
  2. 修饰静态方法,对当前类的Class对象加锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁

2. synchronized 的实现原理

想要了解synchronized 的实现原理,就要先知道Java对象是怎么存放的。因为synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步。


2.1 Java对象组成

Java对象分为三部分:

  1. 对象头,包括**Mark Word (标记字段)** 和 Klass Pointer(类型指针)
    • Mark Word用来存储对象自身的运行时数据
    • Klass Point 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
  2. 实例变量,存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐
  3. 填充字节,由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

image-20230325143946906

Java对象有这三部分,锁就在对象的对象头Mark WordMark Word的结构如下,在64位虚拟机下,MarkWord是64bit大小的,其存储结构如下所示

img

img


2.2 Monitor

Monitor可以理解为是一个同步工具或者同步器,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

ObjectMonitor() {
    
    
    _count        = 0; //记录数
    _recursions   = 0; //锁的重入次数
    _owner        = NULL; //指向持有ObjectMonitor对象的线程 
    _WaitSet      = NULL; //调用wait后,线程会被加入到_WaitSet
    _EntryList    = NULL ; //等待获取锁的线程,会被加入到该列表
}

对于一个被synchronized 修饰的方法和代码块来说

  1. 当多个线程同时访问一个方法时,这些线程会被放入EntryList队列中,此时这些线程处于阻塞(Blocked)状态。
  2. 当一个线程获取到了对象的Monitor后,就进入可运行(running)状态,执行方法,此时ObjectMonitor对象的_owner就会指向当前线程,表示当前线程获取到了锁。并且锁的计数器_count需要加一。
  3. 当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的owner变为null,count减1,同时线程进入WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程进入EntryList队列,竞争到锁再进入_Owner区
  4. 当线程释放锁的时候,线程会释放Monitor对象,锁的计数器_count需要减一,当锁的计数器为0的时候,就会彻底释放锁。

Monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的。


2.3 从字节码角度看synchronized

这里有一个加锁的代码

public class Test {
    
    
    public int count = 0;
    public void addOne() {
    
    
        synchronized (this) {
    
    
            count++;
        }
    }
}

将这个Java程序编译成字节码class文件

 public void addOne();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter // 进入同步方法
         4: aload_0
         5: dup
         6: getfield      #2                  // Field count:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field count:I
        14: aload_1
        15: monitorexit // 退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit // 退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:

可见,字节码底层是通过monitorenter进入同步代码块的,通过monitorexit指令退出同步代码块的。

monitorexit指令有两个,第一个是正常退出同步代码块的情况。第二个则是由于同步代码块出现异常而出现释放锁的情况,这种设计可以有效避免死锁。


3. 锁升级

为什么会出现锁升级呢?

一开始,synchronized 无论是大并发还是小并发都属于重量级锁,效率低下,因为操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

于是,在JDK6之后,在JVM层面对synchronized 进行了优化,为了减少锁的获取和释放所带来的性能消耗,引入了“偏向锁”和“轻量级锁”。也就出现了锁升级的情况。

注意,锁只可以升级但不能降级,但是偏向锁状态可以重置为无锁状态。

img


3.1 偏向锁

偏向锁的出现,是为了应对同一个线程多次获取一个锁的情况的出现,因此没有必要每次都要竞争锁,从而降低获取锁的代价。

偏向锁的核心思想是:如果一个线程获取到了锁,那么就进入偏向模式,此时Mark Word结构也变为偏向锁模式,当这个线程再次来请求获取锁,则无需在任何同步操作,直接获取锁。

加锁的时候,如果该锁对象支持偏向锁,那么Java虚拟机会通过CAS操作,将当前线程的地址也就是线程ID记录到对象头的标记字段,并且将标记字段的最后三位设置为101。

在这里插入图片描述

如果前面通过CAS加锁、解锁的时候,对比当前线程ID和Java对象头的线程ID,如果一直,就可以直接获取锁。

如果不一致,说明存在其他线程需要竞争锁对象,那么就需要查看Java对象头的记录的线程是否存活。

如果没有存活则会将锁对象重置为无锁状态,其他线程都可以竞争将其设置为偏向锁。

如果存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

img


3.2 轻量级锁

轻量级锁考虑的是竞争对象的线程不多,而且线程持有锁的时间也不长的情景。

轻量级锁的获取主要有两个情况

  1. 当偏向锁关闭的时候
  2. 由于多个线程竞争导致偏向锁升级为轻量级锁

线程A在获取轻量级锁的时候,会先把锁对象的Mark Word复制一份给线程A的栈帧中创建的用于存储锁记录的空间(Displaced Mark Word),然后使用CAS把对象头的内存替换成线程A存储的锁记录。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

如果自旋次数到了线程B还没有释放锁,或者线程B还在执行,线程A还在自旋等待,这时又有一个线程C过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

轻量级锁的释放
在释放锁时,当前线程会使用CAS操作将·Displaced Mark Word内容复制回锁的MarkWord里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。

img


参考:

  1. 【JUC】10. synchronized与锁升级_synchronized性能下降_起名方面没有灵感的博客-CSDN博客
  2. 详解Synchronized底层实现,锁升级的具体过程,与Lock的区别 - 掘金 (juejin.cn)
  3. synchronized四种锁状态的升级 - 掘金 (juejin.cn)
  4. 大彻大悟synchronized原理,锁的升级 - 掘金 (juejin.cn)

猜你喜欢

转载自blog.csdn.net/weixin_51146329/article/details/129855577