Monitor和偏向锁、轻量级锁、重量级锁

目录

1、详解对象头结构

2、重量级锁的实现原理

1、monitor

2、给一个对象上锁的原理

3、重量级锁的锁重入

3、synchrozed 的优化

1、偏向锁概述

2、轻量级锁概述

4、总结 syn的锁升级过程


1、详解对象头结构

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

一个普通Java对象的对象头结构:

以32为虚拟机为例

  • Mark Word用于存储对象自身的运行时数据
  • klass word指向对象的类型

 64位虚拟机的mark word占8个字节,它的具体内容:

  • 普通对象:状态位01,偏向标识为0,主要存放对象的hashcode、分代年龄
  • 偏向锁:状态位01,偏向标识为1,存放偏向的线程id、分代年龄
  • 轻量级锁:状态位00,存放对应的lock record锁记录对象在栈上的地址
  • 重量级锁:状态位10,存放对应的monitor的地址
  • 等待被垃圾回收,状态位11

可以看出,对象在作为锁使用时,与该对象的含义、继承关系等都无关,因为锁的底层只在该对象上体现。

注意:一个对象作为synchronized的锁对象,它就不会被垃圾回收,后续作为GC Roots。所以它不需要存储分代年龄。

2、重量级锁的实现原理

1、monitor

monitor,翻译为“监视器”或“管程”。

Monitor对象是操作系统提供的,每个Java对象都可以关联一个Monitor对象,它是用来给对象上锁的,主要分为以下三个部分。

  • WaitSet:存储 之前获取过这把锁,但进入Waiting状态的线程。
  • EntryList:阻塞队列,存放等待获取锁的线程
  • Owner:当前持有锁的线程

另外还有count属性用来表示锁重入。

2、给一个对象上锁的原理

使用synchorzed给对象上重量级锁后:

  • 该对象头的Mark Word中,存放指向关联的Monitor对象的指针。该对象原有的mark word信息保存在monitor中。
  • 本来对象是normal的。
    • 使用synchorzed上锁后,对象状态变为Heavyweight Locked,mark word改为存储Monitor对象的地址。
    • mark word后两位从01(普通对象)变为10(重量级锁)。
  • monitor的owner存储了当前持有锁的线程。

3、重量级锁的锁重入

在monitor中,有一个count属性,用于记录锁重入的次数。

  • 使用synchorzed给对象上锁,对象头的mark word部分存储了一个Monitor对象的地址,此时Monitor的Owner为空,count为0
  • 一个线程尝试获取锁
    • 先判断count属性是否为0,如果为0,说明锁可用。
    • 把Owner设置为 本线程,线程 进入Running 状态。
    • count属性+1
  • 如果count属性不为0
    • 检查锁对象的对象头,发现它是Heavyweight Locked状态,关联了一个Monitor
    • 查看Monitor的Owner属性,如果owner属性指向本线程,说明是锁重入,count+1
    • 如果owner属性指向其他线程,当前线程被阻塞,进入阻塞队列 EntryList,变为 Blocking 状态。
  • 等到持有锁的线程执行完毕,就会唤醒 EntryList 中阻塞的线程来竞争锁。
  • 如果持有锁的线程在运行中调用了wait()方法,就释放锁,进入monitor的等待队列。

syn 修饰的内容如何获取monitor对象

syn 修饰代码块

JVM在需要同步的代码块开始的位置插入monitorentry指令,在同步结束的位置或者异常出现的位置插入monitorexit指令。

JVM确保它们是成对出现的,但是根据程序执行的不同分支,比如正常结束或出现异常,一个monitorentry下面可能有多个monitorexit。但只会执行到一个。

这两条指令依赖于底层的操作系统的Mutex Lock来实现。

使用Mutex Lock需要将当前线程挂起,并从用户态切换到内核态来执行,这种切换的代价非常高。

执行monitorentry,含义是尝试获取monitor的所有权。

syn 修饰方法

方法同步不再是通过插入monitorentry和monitorexit指令实现。

是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的。

如果方法表(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,说明这个方法是一个同步方法。

那么线程在执行方法前会先去获取对象的monitor对象。

如果获取成功则执行方法代码,执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞。

6、重量级锁的缺陷

Java中的线程需要映射到操作系统的原生线程之上。

在早期的Java版本中,synchrozed的底层只能使用monitor,这是依赖于操作系统的Mutex Lock(互斥锁)实现的。

每次对线程的唤醒和阻塞,都必须经由操作系统转入内核态来完成,这是一个重量级操作,效率比较低。

3、synchrozed 的优化

montior 是由操作系统提供的,成本较高。如果每次进入 synchrozed时都需要进入 montior 中获取锁,性能较低。

JDK1.6开始,对synchronized做了优化,引入了偏向锁、轻量级锁。

它们的引入是为了解决,在没有多线程竞争或基本没有竞争的场景下,因使用传统锁机制带来的繁重性能开销问题。

1、偏向锁概述

大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁。

因此,如果每次都要竞争锁,会有很多额外性能损失、为了降低获取锁的代价,引入了偏向锁。

比如一段同步代码块,假如每次都是同一个线程去执行它,其实是线程安全的,没有必要去竞争锁。

偏向锁的优化思想是:

  • 如果使用轻量级锁,每次执行同步代码之前都需要使用CAS操作,效率较低
  • 偏向锁的思想,每次执行同步代码之前只需要检查偏向线程ID即可,需要的CAS操作较少,所以比起轻量级锁效率更高
  • 但是偏向锁在存在多线程执行的情况虾,需要发生偏向锁撤销,这个操作的开销必须小于节省CAS带来的开销,否则这个优化就没有意义

        偏向锁的获取流程

  • 从当前线程的栈中找到一个空闲的Lock Record,指向当前的锁对象
  • 判断锁对象的mark word的低三位是否为101偏向状态,如果是:
    • 判断偏向的线程ID和当前线程ID是否相同
      • 如果相同,就直接执行同步代码块
      • 如果是匿名偏向,即偏向状态但是偏向线程ID为空,就使用CAS将当前线程ID置为偏向线程ID
        • 如果修改成功,就成功获取偏向锁
        • 如果修改失败,就升级为轻量级锁
      • 如果不相同,就查看锁对象头中存储的线程id对应的线程是否存活。
        • 检查偏向线程的状态,如果线程已经死亡,就查看是否允许重偏向
          • 如果允许重偏向,就把锁对象设置为匿名偏向状态,之后进行重偏向
          • 如果不允许重偏向,就把锁对象设置为无锁状态,下次只能直接升级成轻量级锁
        • 如果偏向线程依然存活,就需要在下一个全局安全点STW,查看该线程的栈帧信息
          • 如果该线程不再使用该锁对象,就查看是否允许重偏向
            • 如果允许重偏向,就把锁对象设置为匿名偏向状态,之后进行重偏向
            • 如果不允许重偏向,就把锁对象设置为无锁状态,下次只能直接升级成轻量级锁
          • 如果该线程还需要持有偏向锁,就撤销偏向锁,设置为轻量级锁
          • 具体该线程是否使用偏向锁,可以遍历它的栈帧中的Lock Record对象来判断。
  • 如果不是,就进入轻量级锁的逻辑,构造一个无锁状态的对象头mark word,然后存入Lock Record中

偏向锁解锁后,不会被主动释放:一个线程退出同步代码块后,不会撤销锁对象中自己的线程id。

2、轻量级锁概述

情形:对于一个对象,在多线程场景下,多个线程并不会同时访问它,而是错开访问

产生了两个问题:

  • 通常情况下不存在竞争,此时其实不用加锁。每次线程都要先访问montior,才能执行它,效率就很低。
  • 但是,每个线程在执行它之前应该先检查,它是否正在被其他线程运行,否则会出现问题

思想:

  • 使用某种标记,能快速判断是否有其他线程持有该锁对象,表明当前是否存在竞争关系。如果不存在竞争,说明只有一个线程访问,就不用经过锁
  • 如果发现存在竞争,再去使用锁(轻量级锁升级为重量级锁)

 轻量级锁的获取流程

轻量级锁对于编码人员是透明的(不可见),语法仍然是synchrozed ,底层会自动优化。

Lock Record

锁记录对象,放在线程的栈帧中。

它包含两部分信息:

  • 存放锁对象头的mark word
  • 存放锁对象的地址信息

一个线程试图获取轻量级锁的流程

  • 如果锁对象为 001 无锁状态,就在线程的栈帧中创建一个 Lock Record 锁记录对象。

  • 将锁对象的mark word拷贝到本线程的Lock Record中,称之为 Displaced Mark Word

  • 尝试获取锁

    • 使用CAS操作,试图将锁对象头中mark word的内容,替换成该线程的Lock Record的地址。
  • 如果CAS执行成功,当前线程成功获取锁

    • 对象头的Mark Word中存储了锁记录地址和状态 00 (00表示轻量级锁),表示由该线程给对象加锁
    • 对象头中原先的信息,由锁记录临时保存。等到释放锁的时候,再把数据恢复回去。
  • 如果 cas 替换失败,那么判断对象头Mark Word的“lock record 锁记录地址”,存在两种情况:

    • 锁记录地址指向其他线程,说明是其它线程已经持有了该 锁对象 的轻量级锁,当前线程先自旋。如果达到了自旋次数,或者又有新的线程来竞争锁,就将轻量级锁膨胀为重量级锁,锁对象头部指向一个monitor。

    • 锁记录地址指向此线程,说明是此线程自身执行了 synchronized 锁重入,会再添加一条 Lock Record 锁记录,作为重入的计数。

      这样设计是很有必要的,因为多次持有锁时如果不额外做记录,在释放锁时就会出现问题。只有等到该线程的锁全部被释放,才说明当前线程的操作已经结束,该对象才能被允许再次上锁。

  • 当退出 synchronized 代码块释放锁时,如果有值为 null 的锁记录,表示有重入,这时会删除该条锁记录,表示重入计数减一

    当退出 synchronized 代码块释放锁时,如果锁记录的值不为null,则使用cas,将对象头Mark Word的值恢复

    • 如果操作成功,则成功释放了该对象的锁
    • 如果操作失败,说明该轻量级锁进行了锁膨胀,或者已经升级为了重量级锁,就进入重量级锁的解锁流程。

4、总结 syn的锁升级过程

一个线程要获取一个锁对象的锁:

  • 检查是否是对象mark word后三位状态位是否是001无锁状态,其中第一位表示是否偏向,后两位表示对象的类型
    • 如果是,就设置为101偏向锁,通过CAS将自己的线程id放入对象头的mark word,此时锁对象偏向当前线程
  • 如果对象状态是101,检查mark word的线程id是否和当前线程id相同。
    • 如果相同,不用获取锁,直接执行被锁住的代码
    • 如果不相同,主动检查持有偏向锁的线程状态。如果它已死亡,或者不需要偏向锁了,就将锁对象置为001无锁状态。否则,如果该线程还在使用偏向锁,当前线程就将偏向锁升级为轻量级锁,锁对象状态为00
  • 如果对象状态是00,使用CAS,将锁对象头中mark word的内容,替换成当前线程的Lock Record的地址
    • 如果操作成功,当前线程成功持有轻量级锁
    • 如果操作失败,就尝试自旋。如果自旋到一定次数依然操作失败,或者有别的线程尝试获取轻量级锁,就将轻量级锁升级为重量级锁。申请一个monitor,将对象状态置为10,mark word指向monitor对象,当前线程进入monitor的阻塞队列等待。

锁只能升级不能降级,但是偏向锁状态可以被重置为无锁状态,后续升级成轻量级锁。

原因是,既然锁升级了,那么说明会有高并发的场景出现,那么肯定不能擅自降级,因为不能预测是否还有高并发的时刻。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MonitorJava中的同步机制,用于实现线程之间的互斥和协作。在Java中,每个对象都有一个与之关联的监视器(monitor)。当一个线程需要访问某个对象时,它必须先获得该对象的监视器,进入对象的监视器后,线程才能执行对象的同步方法或同步块。 在Java中,监视器的实现是通过内置(也称为互斥)来实现的。内置是一种可重入的互斥,它可以保证同一时间只有一个线程能够获得,其他线程则被阻塞。 一个线程要获得一个对象的监视器,必须执行以下操作: 1.尝试获取对象的,如果对象的未被占用,则获取,并将的持有者设置为当前线程。 2.如果对象的已经被占用,则当前线程将被阻塞,直到被释放。 3.当线程执行完对象的同步方法或同步块时,它会自动释放对象的。 在Java中,每个对象都有一个监视器和一个等待队列。当一个线程调用对象的wait()方法时,它就会进入等待队列,并释放对象的。当另一个线程调用对象的notify()方法时,它会从等待队列中选取一个线程,将其唤醒,并将对象的交给该线程。 重量级指的是在获取时,如果已经被其他线程占用,则当前线程会进入阻塞状态,直到被释放。这种的开销比较大,因为它需要频繁地进行上下文切换和线程阻塞。 在Java中,当一个线程尝试获取一个对象的时,如果已经被其他线程占用,则该线程会进入阻塞状态,并将自己加入到对象的等待队列中。当被释放时,等待队列中的线程会被唤醒,并竞争的所有权。这种的实现方式称为重量级重量级的解过程比较简单,只需要将的状态设置为未占用,并唤醒等待队列中的一个线程即可。如果等待队列中没有任何线程,则解过程就结束了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值