Synchronized的原理和锁升级过程

Synchronized的原理和升级过程

参考文献:

Java精通并发-通过openjdk源码分析ObjectMonitor底层实现 - cexo - 博客园 (cnblogs.com)

由Java 15废弃偏向锁,谈谈Java Synchronized 的锁机制 - 云+社区 - 腾讯云 (tencent.com)
15.多线程编程中锁的4种状态-无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态_郑学炜的技术博客-CSDN博客

ps:这是对知识点的归纳和自己的一些理解
如果有说的不对的地方,欢迎在评论去指出(侵删)

1、对象布局

首先,我们要知道对象在内存中的布局:

已知对象是存放在堆内存中的,对象大致可以分为三个部分,分别是对象头、实例变量和填充字节

  • 对象头由MarkWord和**Klass Point(**类型指针)组成
    • Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例(一般是4个字节)
    • Mark Word用于存储HashCode、分代年龄、锁标记位、threadID等信息。(一般是8个bytes)
  • 实例变量存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐
  • 填充字符,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的

在这里插入图片描述

在这里插入图片描述

查看对象在内存中的布局和样子:

public class Test2 {
    private static class T{
        int i;
        long l;
        boolean b;
        String s = "Hello world";
    }
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        //这个方法需要下载一个openjdk的包
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
    }
}

在这里插入图片描述

2、Synchronized的原理

通过第一部分可以知道,Synchronized不论是修饰方法还是代码块,都是通过持有修饰锁对象来实现同步,那么Synchronized锁对象是如何实现上锁的呢?

  答案是:经过synchronized的代码,对其锁对象的markword进行了修改,这个修改其实就是使其中的锁信息的指针指向monitor(一般讨论的是重量级锁,偏向锁和轻量级锁有其他方式完成,在下节会说到)

  jvm中的同步是基于进入与退出监视器对象(monitor,也叫管程对象)来实现,每一个对象实例都会有一个monitor对象,monitor对象会和java对象一同创建并和一同销毁,monitor对象是有c++来实现的

​   当多个线程同时访问一段同步代码时,这些线程会被放到一个EntrySet集合中,处理阻塞状态的线程都会被放到该列表当中。接下来当线程获取到对象的monitor时,monitor对象中的_owner属性指向持有monitor的线程,monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有mutex,这时其他线程就无法再获取到该mutex

对象头----> monitor ----操作系统层面----> mutexlock锁
下面是c++实现的monitor对象:
在这里插入图片描述

​   如果线程调用了wait()方法,那么该线程就会释放掉所持有的mutex,并且该线程会进入WaitSet集合(等待集合)中,等待下次被其他线程调用notify/notifyAll唤醒。如果当前线程顺利执行完毕方法,那么它会释放掉所持有的mutex。

​   总结一下:同步锁再这种实现方式中,因为monitor依赖于底层的操作系统实现,这样就存在用户态和内核态之间的切换,所以会增加开销

证明:

public class Test2 {
 private static class T{}

 public static void main(String[] args) throws InterruptedException {
     T t = new T();
     System.out.println(ClassLayout.parseInstance(t).toPrintable());
		//t为锁对象,两次markword不一样
     synchronized (t){
         System.out.println(ClassLayout.parseInstance(t).toPrintable());
     }
 }
}

在这里插入图片描述

synchronized的几个状态和升级的锁的类型(即markword中的锁信息的内容):

在这里插入图片描述

在jvm指令的层次上讲(成对出现):

方法块方式:

monitorenter : 加锁,进入monitor对象

monitorexit :解锁,退出monitor对象

对于monitorenter:

  • 每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter指令时会尝试获取相应对象的monitor,获取规则如下:
    • 如果monitor的进入数(count)为0,则该线程可以进入monitor,并将monitor进入数(count)设置为1,该线程即为monitor的拥有者。
    • 如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数(count)加1,所以synchronized关键字实现的锁是可重入的锁。
    • 如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数(count)为0,再重新尝试获取monitor。

对于monitorexit:

  只有拥有相应对象的monitor的线程才能执行monitorexit指令。每执行一次该指令monitor进入数(count)减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。

修饰符方式

ACC_SYNCHRONIZED标志设置

本质也是对于对象监视器 monitor 的获取与释放

3、锁升级

为什么要锁升级?
  阻塞或唤醒一个Java线程需要操作系统切换CPU状态(用户态转到内核态)来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长

jdk1.6之后添加了很多细节,其中就有对于锁的:

Synchronized的升级优化的过程:

  1. 无锁
  2. 首先有一个线程到来,先尝试着给这个线程加一个偏向锁
  3. 如果有另外一个线程来竞争,那么这个锁就会升级为轻量级锁(多数情况下是自旋锁)
  4. 并发数越来越高,一个线程自旋了很长时间(一般是10次)还没拿到锁,就再次升级为重量级锁(悲观锁)

原理:

synchronized 锁升级原理:

  1. 在锁对象的对象头里面有一个 threadid 字段,thread1访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为当前线程 id

  2. 线程2到来的时候会先判断 threadid 是否和thread1的 一致,如果一致则可以直接使用对象锁

  3. 如果不一致(表示线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID)

    • 查看java对象头中记录的treadid是否存活,如果没有存活,那么锁对象先被重置为无锁状态,则线程2将其再次变为偏向锁,随后存储线程2的id为threadid
    • 如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁(通过自旋持续尝试获取锁对象),第一次升级会在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,使得markword中的轻量级锁的指针指向这块空间
  4. 自旋一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁

(必看)----------------------------------------------------------

  • 首先对象头中的markword信息:
    在这里插入图片描述

  • 偏向锁

    偏向锁加锁过程:

      当一个线程再次访问这个同步代码或方法时,该线程只需去对象头的 Mark Word 判断是否有偏向锁指向它的 ID,无需再进入 Monitor 去竞争对象了。当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是 01,“是否偏向锁”标志位设置为 1,并且将threadid设置为当前线程 ID,表示进入偏向锁状态。

      一旦出现其它线程竞争锁资源,偏向锁就会被撤销。撤销时机是在全局安全点,暂停持有该锁的线程,同时检查该线程是否存活。如果存活,则撤销偏向锁升级为轻量级锁;如果死亡则先将锁对象设置为无锁,在由新线程升级为偏向锁

    为什么要加偏向锁?(偏向锁解决什么问题?)

      因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁减少锁竞争的资源开销,才引入的偏向锁。

    jdk15偏向锁为什么被废除?缺点是什么?

      偏向锁的缺点:在高并发场景下,大量线程同时竞争同一个锁资源,偏向锁会被撤销,发生 stop the world后,开启偏向锁会带来更大的性能开销(这就是 Java 15 取消和禁用偏向锁的原因)

  • 轻量级

    轻量级锁的上锁过程

    (1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图所示。
    在这里插入图片描述
    (2)拷贝对象头中的Mark Word复制到锁记录中。
    (3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果自旋多次更新成功,则执行步骤(4),否则执行步骤(5)。
    在这里插入图片描述
    (4)**如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,**即表示此对象处于轻量级锁定状态.
    (5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

    为什么要引入轻量级锁?

      轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,因为时间也不长,倒不如让其自旋一会,追求速度

  • 重量级锁

      如果自旋的时间太长也不行,因为自旋是要消耗CPU的,自旋的次数是有限制的重量级锁把除了拥有锁的线程都阻塞,防止CPU空转消耗cpu资源。

      重量级锁通过对象内部的监视器(monitor)实现,需要从用户态切换到内核态,切换成本非常高

    当持有锁的时间够长时,可以使用重量级锁。

总结图
在这里插入图片描述

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值