Java6对synchronized的优化-锁升级过程详细过程

Posted 敲代码的程序狗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java6对synchronized的优化-锁升级过程详细过程相关的知识,希望对你有一定的参考价值。

前言

以前我们在复习synchronized的时候,和lock一对比,总说它是一个重量级锁,性能很差,不如lock来的方便,但其实synchronized作为Java中元老级的关键字,很多jdk原始代码都有用到它,因此Java开发团队还是非常中意这个“私生子”的,所以为了给它减重,开发者也早在Java6就为synchronized专门设计了一套优化处理过程。

一、synchronized使用

  1. 加到方法上,锁就是这个对象,防止多个线程同时访问这个对象的synchronized方法;
  2. 加到静态方法,锁就是这个类的class对象,防止多个线程同时访问这个类的synchronized方法,对该类所有对象适用;
  3. 加到代码块,锁就是该代码块所在的对象,防止多个线程同时访问该代码块;

二、JVM对synchronized的实现方式

  1. 代码块同步:通过使用 monitorentermonitorexit 指令实现的
  2. 同步方法:ACC_SYNCHRONIZED 修饰

三、Java对象头(存储锁类型)

了解锁升级之前,必须先知道Java对象头,正式对象头记录的信息才实现了锁升级过程中的线程判断。

Java对象:

Java对象头:包含两部分:MarkWord 和 类型指针

MarkWord:Mark Word 用于存储对象自身的运行时数据,如 HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等。

占用内存大小与虚拟机位长一致(32位JVM -> MarkWord是32位,64位JVM -> MarkWord是64位)

四、锁升级过程

无锁状态 -> 偏向锁 -> 轻量级锁(自旋锁,自适应自旋) -> 重量级锁

各种锁状态下mark word的内容:

状态偏向锁位标志位存储内容
未锁定001对象哈希码、对象分代年龄
偏向锁101偏向线程ID、偏向时间戳、对象分代年龄
轻量级锁定000指向锁记录的指针
膨胀(重量级锁定)010执行重量级锁定的指针
GC标记011空(不需要记录信息)

无锁状态 -> 偏向锁

首先当处于无锁状态时,MarkWord中存储的是对象的hash code等信息,偏向锁为0,此时线程1进入,发现没有记录线程ID信息,则直接CAS操作将自己的线程ID设置进去,设置成功则获取到偏向锁,偏向锁标志为改为1;失败则要升级为轻量级锁;

线程1执行完后,将markWord中的线程ID设置为空,即释放偏向锁;

如果在线程1释放之前,此时线程2进入,先查markWord中记录的线程ID是否是自己的(这也是偏向锁适用的场景:经验表明,大多数情况下,都是同一个线程进入同一个同步代码块),若是,则获取锁成功;

不是,则说明已经偏向给了其他线程,即出现了线程竞争的情况,此时一般情况下,偏向锁是不可以重偏向的,所以需要升级,如果是可以重偏向的,则会尝试CAS操作替换markWord中的线程id,成功则偏向给线程2,失败则进行锁升级;

偏向锁在升级前,需要先将偏向锁撤销。

如果此时已经偏向给了某个线程,则需要根据markWord中记录的线程id查询该线程是否存活,不存活,则进入无锁状态,或者可重偏向状态,存活则撤销偏向锁,将锁对象的markWord复制到线程栈中,开始升级

偏向锁 -> 轻量级锁

出现两个及以上线程竞争时(已经偏向给了一个线程,此时又出现了另外一个线程竞争,即升级),开始升级为轻量级锁。

加锁时,线程会在自己的栈帧中创建一个Lock Record锁记录(会将对象原来的markWord内容复制过来,作为一个lockRecord,包含对象分代年龄等信息,而原来的markWord只会有一个指针),同时CAS将锁对象的markWord中替换为指向该lockRrecord的指针,操作成功则获取到轻量级锁;

线程执行完,解锁时,只需要移除栈帧中的lockRecord即可,而不用修改markWord中的内容。

轻量级锁 -> 重量级锁

设置失败,则证明有竞争,此时会进入自旋状态。

线程超过10次自旋, 或者自旋线程数超过CPU核数的一半,(JDK1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制)。就会升级为重量级锁。

升级重量级锁:

向操作系统申请资源,CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间

所以升级为重量级锁后,用户线程被系统线程挂起,需要从用户态内核态的转换。这也是最开始说synchronized重,性能差的原因!

总结:

  1. synchronized锁升级过程是单向的,不可逆
  2. 升级过程围绕锁对象中markWord的内容变化;
  3. 每种锁都适应不同的场景,比如偏向锁适用于同一个线程多次进入同步代码的情况,轻量级锁适用竞争小,且同步执行时间短(自旋一会儿就能获取到锁)的情况
  4. 对了,synchronized是可重入的,这点不光可以从偏向锁看出来,即使升级到重量级锁,也是支持的

以上是关于Java6对synchronized的优化-锁升级过程详细过程的主要内容,如果未能解决你的问题,请参考以下文章

Java6中对synchronized的优化

开启偏向锁一定性能更好吗?

Java偏向锁实现原理(Biased Locking)

synchronized关键字

synchronized的锁优化是怎么处理的?

图解 synchronized 的锁升级机制