Synchronzied 的深入理解(下)

Sychronized深入分析

话接上文,我们需要想要知道为什么所有的对象都可以成为sychronized的锁,然后有一张描述对象在内存中的布局。

在这里插入图片描述

首先对于Java而言,我们创建实例的方法可以通过new一个对象,而对于JVM而言,这句话会被解释成,创建一个instanceOopDesc对象,虚拟机使用OOP-KLASS模型来描述Java对象实例。

  • OOP(Ordinary Object Point)指的是普通对象指针
  • Klass 用来描述对象实例的具体类型。

而虚拟机如果描述普通对象会使用instanceOopDesc而数组类型则使用arrayOopDesc来描述,同时他们都继承自OopDesc,由于底层使用c语言编写,这边就不再展开。接下来就是重点戏,我们将目光锁定在OopDesc中,这里面有两个重要的成员,分别是_mark和_metadata

  • _mark 表示对象标记,是markOop类型,也就是真正用来记录锁标记的Mark Word部分。
  • _metadata 表示类元信息,存储一些对象指向他的类元数据( Klass)。

MarkWord在Mark word中定义了可以存储锁状态的常量,结合之前的话题来将,我们每一个使用new关键字创建出来的,并且由于虚拟机解释成instanceOopDesc对象,都会含有这样一个mark word头,接下来的一系列操作都与这个东西有关。虽然,mark word在32位操作系统和64操作系统表现分别为占32bit和64bit,但是总体的布局是没什么区别的。当锁的状态发生变化的时候,相应markdown中的锁标志位也会发生变化。下面以32bit的markdown作为例子。

在这里插入图片描述

现在,来稍微回顾一下,我们现在正在探究sychronized关键字底层到底是如何加锁的,我们从JVM的层面了解到sychronized关键字的上锁对象可以是任何的对象,然后我们接着明白,在JVM中,new出来的对象将会被解释成 instanceOopDesc或者arrayOopDesc,由于这两者都会继承自oopDesc,所以更多的信息也许可以从父类看起,在父类中,我们终于找到了和锁有关系的标记->_mark,也是Mark Word

markDown中存在四种锁状态

  • 无锁
    • 暂未有线程获得锁
  • 偏向锁
    • 已经有一个线程进入了同步块时候,锁标志位将会被设置为01,并记录当前已经持有偏向锁线程的Id。
  • 轻量级锁
    • 当已有线程获得偏向锁后,仍有线程尝试获得锁的时候,锁会升级为轻量级锁
  • 重量级锁
    • 当因为锁等待的线程过多,轻量级锁将会膨胀为重量级锁,造成阻塞。

而修改这些锁状态的对象,被称为monitor(监视器对象),他是一个同步对象,任何的Java对象天生就会携带这样一个monitor,所以,多线程去访问被sychronized修饰的方法,或者代码块时,本质上是去争夺对象监视器,然后去修改此对象的锁标记,来达到锁的升级,和独占。

Sychronized 锁升级

上面我简单的解释了一下四种锁的状态,为什么要提出这四种锁的状态呢,明明锁只要一种就可以了不是吗,让其他未获得锁的线程等待不就可以了吗?

早期的Sychronized确实是只有重量级锁,可是后来设计者发现,大部分的线程在获得锁后,会在极短的时间内释放掉,也就是变为无锁状态,明明只要在门口稍微等待一下,我就可以进去了,你却还要阻塞我,这不是有点不太友好呢?

所以,为了优化Sychronized锁的性能,设计者提出了其它的两种锁,偏向锁和轻量级锁。

我们举一个例子。

偏向锁的场景是:线程A进入了同步块,他将会把自己的线程Id挂到偏向锁上,并且设置锁标记为01,表示现在已经有人了。

轻量级锁的场景是:同时来了线程A和线程B,线程A优先于线程B获得了偏向锁进入了同步块,线程B由于没有获得锁,锁将会被升级成轻量级锁。

重量锁的场景是:偏向锁和轻量锁的场景都算不上真正意义上的上锁,而只是修改了锁的标志位,并没有真正的阻塞,当有更多的线程在等待获得锁,并且超过了自旋时间的话,轻量锁将会膨胀为重量级锁,并将后续的线程阻塞。

在这里插入图片描述

偏向锁的获得

我们可以作锁对象(monitor)中的锁一开始应该是无锁状态,因为这个时候,还没有任何线程去获得这个对象监视器

  • 线程1去判断对象中的Markword是否处于可偏向状态
    • 如果是可偏向状态,则通过cas操作,将当前线程ID写到Mark Word中去。
      • 如果cas成功,那么markword会带有当前线程的ID并执行后续的同步块
      • 如果cas失败,说明这个时候已经有其他线程获得了偏向锁,这种情况下当前锁就存在竞争,以线程2的视角就是线程1获得锁,而导致线程2无法获得偏向锁。这种情况下,线程2会去尝试撤销偏向锁,并且将它持有的锁升级为轻量级锁,不过这个升级操作需要等待没有线程在执行字节码的时候,才能去做。
    • 如果是已偏向状态,则需要检查markword中存储的线程ID是否等于当前线程的ThreadID
      • 如果相等,不需要获得锁,可在此执行同步代码块。
      • 如果不相等,说明锁偏向于其它线程,需要撤销偏向锁升级到轻量级锁。

偏向锁的撤销

偏向锁的撤销并不是意味着对象恢复成无锁可偏向状态,也就是对于偏向锁的撤销这一概念而言,所谓的撤销,也就意味着下一步可能会被升级为轻量级锁状态。

上图,还是用线程1和线程2来举例,线程2发现获取偏向锁中,cas失败时,会直接将被偏向的锁对象升级到被加了轻量级锁状态。

而对于线程1这个已经持有了偏向锁的线程而言,他要进行偏向锁的撤销操作时,会有两种情况。

  • 线程1已经将同步代码块执行完毕,线程1将会把锁对象的头设置为无锁并且线程2可以cas操作将已经设置成无锁的锁对象偏向自己。
  • 线程1的同步代码块还未执行完毕,这个时候,线程1将会把偏向锁审计为轻量级锁后,再执行后面的同步代码块。

对于偏向锁的总结

偏向锁算是一种乐观锁的实现,他的升级操作只能由当前已经获得了锁的线程来执行。

那么在实际开发中,一般都会存在2个以上线程进行竞争,所以开启偏向锁的意义不大,反而会影响性能,所以我们可以通过JVM参数去关闭他,UseBiasedLocking

轻量级锁

由于种种原因,我们的偏向锁被升级为轻量级锁,那么我们选取一个比较容易理解的场景,在偏向锁升级为轻量级的场景下,是不是意味着至少有两个线程存在锁的竞争,这里的两个线程表示一个线程获得了锁另一个线程没有获得锁情况。

很常见的就是线程1获得偏向锁后,还未执行完同步块,这个时候线程2来了,希望获得锁,但是这个时候cas操作失败,导致线程1将偏向锁升级为了轻量级锁。

可是这个时候,线程2并没有放弃,他还是希望获得锁,即便这个锁已经变成了轻量级锁,如果第一次尝试获得轻量级锁失败(其实这一操作也就是用cas再次尝试替换markword的值),jvm会给他另一个建议,那就是,要不你再试试?的建议,所以线程2将会用自旋锁的方式,多次去尝试去获得锁。

这里的自旋不过是一段无意义的死循环,目的是不希望线程2因为第一次尝试没有获得锁而被挂起,他可能只需要多尝试几次就可以再次获得锁了。

所以,自旋锁的使用场景在于同步块可以执行的非常快,以至于我只要自旋几次就能够撑到获得锁的线程1,离开同步代码块,并且线程2通过cas操作成功将轻量级锁获得。

当然上面是比较理想的情况,如果这个时候,连自旋都无法让线程2获得锁的话,那么轻量级锁将会膨胀为重量级锁并阻塞线程2,毕竟自旋也是比较耗费cpu性能,我不能让你一直尝试这种操作,所以无可奈何,只能将你阻塞了。

在这里插入图片描述

重量级锁

这个时候你可能会有疑问,为什么这里修改锁标志的会是未获得锁的线程,不是说只有获得锁的线程才有资格去修改锁的标志位了,来控制他的升级。

这里就不得不拿出,我们在文章开头谈及到的对象监视器(Monitor)了,对象监视器就是为了重量级锁而存在的,对象监视器也可以叫做同步对象,同时所有的对象天生就携带了monitor,那么就意味着,如果事态已经严重到需要重量级锁出面的地步的话,原有的只是修改markword已经无法满足需求了,所以monitor将会出面维护后续阻塞的线程。

public class SychronziedDemo {

    public static void main(String[] args) {

        synchronized (SychronziedDemo.class){

        }

    }

}

我们可以写一个这样简单的代码块,然后查看.class到底生成了怎样的字节码指令来表示这个代码块。

在这里插入图片描述

加了同步代码块后,我们在字节码中发现了一个monitorentermonitorexit

从这里开始,我们的sychronized会真正意义上的去阻塞线程了,而monitor将会去维护这些被阻塞的线程。
在这里插入图片描述

到此,sychronized所有锁分析完毕。

回顾

我们来做一个简单的总结

 synchronized (lock){
	// todo
}

其实上述说了那么多,也就是对这一句话的解释,因为这句话很简单,所以底层为我们做了非常多的操作。

倘若现在存在若干个线程去访问这个同步代码块,线程A,线程B,线程C…

  1. 只有线程A会进入临界区(快要进入同步块的地段)
  2. 线程A和线程B交替进入临界区,竞争还行,算不上激烈。
  3. 线程A,线程B,线程C…非常多的线程同时进入临界区,竞争激烈。

偏向锁

线程A进入临界区,将锁对象(lockObject)的对象头 Mark Word的锁标志位设置为01,同时cas将自己的线程ID记录到Mark Word中,进入偏向模式。这之后若线程A再次进入同步块的时候,无需同步操作,就可以进入同步块。

轻量级锁

很显然,偏向锁太过理想化,如果只有一个线程,我们其实不加锁效率会更好。

当线程A和线程B交替进入临界区的时候,因为线程A优先于线程B获得了偏向锁,而线程B再次尝试获得锁的时候,由于线程A还未执行玩同步块,所以他会暂停自己并且升级偏向锁到轻量级锁。同时线程B将会通过自旋转获得轻量锁。

重量级锁

两个线程可能轻量级锁可以解决,但是更多的线程显然就不能满足锁的需求了。

当更多的线程同时进入临界区的时候,轻量锁将会膨胀为重量级锁,因为这个时候,其它的线程会在被monitor维护的同步队列中阻塞,所以,原轻量级锁获得者,也就是现在重量级锁持有者将会把markword的所标记正式更新为10。

在这里插入图片描述

而接下来,将会是monitorexitmonitorenter的流程,将会被阻塞接管。

每天积累一点点

文章代码

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值