synchronized锁升级详解

synchronized锁升级

  • 在JDK1.5之前,我们要想实现线程同步,只能通过synchronized关键字这一种方式达成。synchronized关键字是JVM实现的一种内置锁。从底层角度来说,这种锁释放和获取都是jVM帮我们隐式实现的。
  • 从JDK1.5开始并发包引入了Lock锁,Lock锁是基于Java实现的。因此锁的获取和释放都是由java代码实现的。然而synchronized是基于操作系统底层的Mutex Lock来实现的。每次获取释放锁都会带来用户态和内核态的切换,了解操作系统相关的基础知识都清楚,这种切换是非常消耗cpu性能的,它需要保存和恢复一些状态数据等,同时也影响到锁的性能。如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PpsIX3x7-1581229011205)(https://i.loli.net/2020/01/31/xXvBNTFRVEhcGap.jpg)]

  • 从JDK1.6开始synchronized锁的实现发生了很大的变化,jvm引入了相应的优化手段来提升synchronized锁的性能。这种优化涉及到偏向锁,轻量级锁,和重量级锁,从而减少锁竞争带来的用户态和内核态之间的切换。**锁升级的出发点就是减少户态和内核态之间的切换。**尽量使程序一直出入用户态。锁的优化实际上是通过java对象头上一些标志位来实现的。

对象实例

在JDK1.6开始,对象实例在堆中会被划分三个组成部分:对象头,实例数据,和对其填充。

  • 实例数据:对象的相关属性
  • 对其填充:确保数据长度一致。有些没有数据的自动填充一些空间。

对象头

  • Mark Word
  • 指向类的指针
  • 数组长度

我们在锁升级的过程中只需要关注Mark World(它记录了对象,锁和垃圾回收相关的信息,在64位JVM中其长度为64bit)的位信息包括了如下组成信息:
- 无锁标记: 当前对象没有上锁。
1. 偏向锁标记:
2. 轻量级锁标记:
3. 重量级锁标记:直接从用户态切换到内核态。
4. GC标记: 判断对象是否可被垃圾收回收掉。

  • 对于synchronized锁来说,锁的升级主要是通过Mark World的锁标记位与是否是偏向锁标志来达成的。synchronized关键字所对应的锁都是从偏向锁开始的,随着锁竞争的不断升级逐步演化至轻量级锁,最后则变成重量级锁。

偏向锁

  • 总体而言如下图所示:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pD1uaEgk-1581229011207)(https://i.loli.net/2020/02/08/4i85z3kNJTIZUpn.jpg)]

  • 偏向锁的设置

    • 针对一个线程来说,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象就会在其Mark World的将偏向锁进行标记,同时还会有一个字段来存储该线程的Id,当这个线程再次访问同一个synchronized方法时,它会检测这个对象的Mark World的偏向锁已经是否指向了该线程Id,如果是的话那么该线程就无需在进入管程(内核态),而直接进入到该方法,这些操作都是在用户态进行的。
  • 偏向锁的撤销

    • 偏向锁使用了一种等到竞争才释放锁的机制,所以上述的偏向锁的设置只是针对单个线程而言。 当其他线程线程B尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,偏向锁的释放,需要等待全局安全点(这个时间点上没有正在执行字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程释放存活,如果线程不处于活动状态,则对象头设置无锁状态,如果线程仍然活着,拥有偏向锁的栈就会被执行,遍历偏向锁对象的锁记录,栈中的锁记录和对象头的Mark World要么重新偏向其他线程,要么恢复成无锁(换句话说就是持有偏向锁的线程不存活就对象头设置无锁状态,如果线程存活就执行完,要么对象头设置无锁状态,要么偏向其他线程)。
  • 偏向锁的关闭

    • 偏向锁在Java6和java7里默认是开启的,但是它是应用程序启动几秒之后才会激活,可以通过-XX:BiasedLockingStartuoDelay=0 关闭延迟。通过 -XX:-UserBiasedLocking=false 关闭偏向锁,那么线程就会默认进入轻量级锁的状态。

    在偏向锁的应用场景主要集中在竞争不激烈的情况下,通过使用偏向锁可以减少其在CAS操作下的同步性能消耗,从而获取性能的提升。

轻量级锁

  • 若一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试争抢该对象的锁,由于该对象已经被第一次线程获取到了,因此它是偏向锁,而第二个线程在争抢时,发现对象头的Mark World已经是偏向锁,但里面存储的线程Id并不是自己(是第一个线程),那么它会进行CAS,来竞争锁,这里存在两种情况:

    • 获取锁成功:那么它会直接将Mark World中的线程Id由第一个变成自己的,但是锁标志还是偏向锁。
    • 获取锁失败:则表示这时可能会有多个线程同时在尝试争抢该锁,那么这时偏向锁就会升级,升级为轻量级锁。
  • 上述情况如果获取锁失败的情况:

    • 自旋:当发生对Monitor竞争时,若持有者在很短的时间内释放掉锁,则那些在竞争的线程就可以稍微等一下(自旋),在持有者释放锁之后,竞争者就会获得到monitor对象,从而避免了系统的阻塞(不用从用户态切换到内核态进行阻塞等待)。不过当持有者只有锁的时间比较久的时候超过了自旋的时间上限(这个jvm指定的时间点,也不能一直自旋下去浪费cpu资源),这个时候就会停止自旋进入阻塞状态。总体思路就是:先自旋,不成功再去阻塞,尽可能的降低阻塞(用户态和内核态的切换)的可能性,这对那些执行时间比较短的同步锁性能得到了极大的提升。
    • 在了解了自旋的原理之后,如果上述获取锁失败了,就会进入自旋,自选失败了就会进入阻塞状态,这个时候的锁就是重量级锁,因为发生了用户态到内核态的切换。

    重量级锁

    • 就是没升级之前的锁的状态,每次获取锁就要发生用户态和内核态之间的切换。

    各自的优缺点

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QAoJeZGe-1581229011208)(https://i.loli.net/2020/02/09/iv1uBdbIx9M8CSD.jpg)]

    扫描二维码关注公众号,回复: 9641273 查看本文章

参考文献

  • java并发编程的艺术 - 方腾飞
发布了59 篇原创文章 · 获赞 30 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_33249725/article/details/104234812