【Java并发编程】synchronized(六):锁膨胀原理分析(JDK6后)

前言:【Java并发编程】synchronized(二):重量级锁原理分析(JDK6前)

JDK6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁”:锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。

在对象头的 Mark Word 中保存了不同的锁状态:

在这里插入图片描述

  • 偏向锁:加锁信息。通过修改 ThreadId 来实现加锁
  • 轻量级锁:指针,指向拿锁线程的 Lock Record(里面保存了对象的 MarkWord)。通过修改指针来实现加锁
  • 重量级锁:指针,指向对象的 Monitor 对象。通过修改 ObjectMonitor 的 count(++)来实现加锁

1.偏向锁

  • 特点:无竞争(只有一个线程、多个线程但不同时执行)
  • 原理:CAS
  • 注:偏向锁在 Java 6 和 Java 7 里是默认启用的。由于偏向锁是为了在只有一个线程执行同步块时提高性能,如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

1.1 获取锁过程

1)访问 Mark Word 中偏向锁的标识是否设置成1,锁标志位是否为 01——确认为可偏向状态。

2)如果为可偏向状态,则测试线程 ID 是否指向当前线程

  • 如果是,进入步骤(5)
  • 否则进入步骤(3)

3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。

  • 如果竞争成功,则将 Mark Word中 线程 ID 设置为当前线程ID,然后执行(5)
  • 如果竞争失败,执行(4)

4)如果CAS获取偏向锁失败,则表示有竞争(CAS 获取偏向锁失败说明至少有过其他线程曾经获得过偏向锁,因为线程不会主动去释放偏向锁)。当到达全局安全点(safepoint)时,会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着

PS:因为可能持有偏向锁的线程已经执行完毕,但是该线程并不会主动去释放偏向锁

  • 如果线程不处于活动状态,则将对象头设置成无锁状态(标志位为“01”),然后重新偏向新的线程;
  • 如果线程仍然活着,撤销偏向锁后升级到轻量级锁状态(标志位为“00”),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。

5)执行同步代码。

1.2 释放锁过程:

如上步骤(4)。偏向锁使用了一种等到竞争出现才释放偏向锁的机制:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁

偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

2.轻量级锁

  • 特点:轻量并发(线程少,任务执行快(当任务<时间片,宏观相当于多线程交替执行))
  • 原理:自旋 + CAS
    • 自适应自旋:一般10次
    • 自定义自旋次数:设置 preBlockSpin
  • 自旋锁(忙等)
    • 引入自旋锁的原因:互斥同步对性能最大的影响是阻塞的实现,因为挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来很大的压力。同时虚拟机的开发团队也注意到在许多应用上面,共享数据的锁定状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。
    • 自旋锁:让该线程执行一段无意义的忙循环(自旋)等待一段时间,不会被立即挂起(自旋不放弃处理器额执行时间),看持有锁的线程是否会很快释放锁。自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启;在JDK1.6中默认开启。
    • 自旋锁的缺点:自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好;反之,自旋的线程就会白白消耗掉处理器的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,例如让其循环10次,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起(进入阻塞状态)。通过参数-XX:PreBlockSpin可以调整自旋次数,默认的自旋次数为10
    • 自适应的自旋锁:JDK1.6引入自适应的自旋锁,自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:如果在同一个锁的对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。简单来说,就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
    • 自旋锁使用场景:从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。(见前面“轻量级锁”)

2.1 获取锁过程:

线程在执行同步块之前,JVM 会先在当前的线程的栈帧中创建一个 Lock Record,其包括一个用于存储对象头中的 mark word(官方称之为 Displaced Mark Word)以及一个指向对象的指针。下图右边的部分就是一个 Lock Record。

在这里插入图片描述
1)在线程栈中创建一个 Lock Record,将其 obj(即上图的 Object reference)字段指向锁对象。

2)通过 CAS 尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁,然后对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。如果失败进入步骤3

3)如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧

  • 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置 Lock Record 第一部分(Displaced Mark Word)为 null,起到了一个重入计数器的作用。然后结束
  • 如果不是,则说明当前时间锁对象已被某个线程占有,那么此时当前线程只有通过自旋的方式去获取锁。

4)如果在自旋一定次数后仍为获得锁,那么轻量级锁将会升级成重量级锁。即将对象头中锁标志位将变为 10(重量级锁),MarkWord 中存储的也就是指向互斥量(重量级锁)的指针。

2.2 锁释放过程:

1)遍历线程栈,找到所有 obj 字段等于当前锁对象的 Lock Record

2)如果 Lock Record 的 Displaced Mark Word 为 null,代表这是一次重入,将 obj 设置为 null 后 continue

3)如果 Lock Record 的 Displaced Mark Word 不为null,则利用 CAS 指令将对象头的 mark word 恢复成为Displaced Mark Word。如果成功,则 continue,否则膨胀为重量级锁

3.重量级锁

只有重量级锁才能叫真正意义的锁,偏向锁与轻量级锁相当于无锁(并无线程被阻塞让出CPU)

  • 特点:存在并发,且线程执行任务时间长
  • 原理:就是文章前一部分的 monitor, 每一个 JAVA 对象都会与一个监视器 monitor 关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被 synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。

依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能

总结:三种锁的 MarkWord 变化

下图用一张图总结一下锁膨胀过程中Mark Word 的变化情况:

1)一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。

2) 一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了。检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程。如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁;如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

3)轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

文章最后放两个链接,对synchronized在JVM具体实现感兴趣的童鞋有必要一读 死磕Synchronized底层实现–概论Synchronized原理剖析 !!!

猜你喜欢

转载自blog.csdn.net/weixin_43935927/article/details/114047971