synchronized锁的升级过程详细介绍,轻量级锁重量级锁偏向锁无锁

01 优化一

优化之后和Lock效率差不多
重量级锁,假如锁竞争激烈,性能会急剧下降,涉及用户态(用户正在使用)和内核态间的转变
在Java6通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

synchronized监视器锁在互斥同步上对性能的影响很大。Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,需要用户态和内核态之间的转换,状态转换需要花费很多的处理器时间,大大消耗性能,因为用户态和内核态都有自己的内存空间,专用的额寄存器等,用户态切换到内核态需要传递给许多变量参数给内核,内核也需要保护好用户态在切换时的一些寄存器值变量值,这种锁机制也被称为重量级锁,如果同步代码的内容比较简单,切换的时间比代码执行的时间还要长
jdk1.6后对synchronized进行优化,减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,协调线程安全性和性能的平衡,这种优化主要解决上下文频繁切换,由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间。

在这里插入图片描述

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。JDK 1.6中默认是开启偏向锁和轻量级锁的,也可以通过-XX:-UseBiasedLocking来禁用偏向锁。

锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

无锁状态

偏向锁状态
一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁状态
当锁是偏向锁时,被另一线程所访问,偏向锁就好升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提供性能。
重量级锁状态
当锁为轻量级锁时,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数后还没获取到锁,就进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
重量级锁需要通过操作系统在用户态与核心态之间切换,就像它的名字是一个重量级操作,这也是synchronized效率不高的原因


01 无锁

对象锁没有被线程拥有,最后两位是01

在这里插入图片描述

下面函数打对象头信息

Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());

第一行和第二行是对象mark Word,是8个字节,每8位是一个字节,每8位倒着看,每8位里面又是从左到右看,只有第一个8位的后面的两位是01,是无锁
hashCode在不调用的时候是空的,所以hashCode的占位为空

java.lang.Object object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
0         4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4         4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8         4        (object header)                           28 0f ff 7f (00101000 00001111 11111111 01111111) (2147421992)
12        4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

如果调用hashCode

Object object = new Object();
System.out.println("======二进制" +Integer.toBinaryString(object.hashCode()));
System.out.println("======十六进制" +Integer.toHexString(object.hashCode()));
System.out.println(ClassLayout.parseInstance(object).toPrintable());

======十进制21685669
======二进制1010010101110010110100101
======十六进制14ae5a5
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 a5 e5 4a (00000001 10100101 11100101 01001010) (1256563969)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 28 0f ff 7f (00101000 00001111 11111111 01111111) (2147421992)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


如果把偏向锁的延迟时间设置为0(或者4秒后再进行锁竞争,因为默认偏向锁是4秒开启), 哪怕没有synchronic也直接是偏向锁,记录线程id的位置为空
-XX:BiasedLockingStartupDelay=0

public static void main(String[] args) {
    Object object = new Object();
    System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
java.lang.Object object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8     4        (object header)                           28 0f ff 7f (00101000 00001111 11111111 01111111) (2147421992)
12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

01 偏向锁

当有线程获取对象锁时,从无锁变为偏向锁,把线程ID设置为自己的,是否偏向锁改为偏向锁,使用的是cas更新

 大多数情况下锁不仅不存在多线程竞争,且总由同一个线程多次获取,不需要重量级锁的操作系统的用户态和内核态之间的切换,偏向锁让线程获得锁的代价更低,如果是同一个线程就消除了同步,先检测锁标记位是否是偏向锁,再检查mark word里面的线程id是不是自己,如果是就不需要再加锁释放锁,直接进入同步,不需要cas去更新头部信息,偏向锁几乎没有性能开销,性能很高,如果mark word里面的线程id不是自己的,会cas去更新为自己,如果更新成功,则偏向锁又会偏向于自己线程,锁不会升级,如果更新失败,有可能会升级为轻量级锁
 偏向锁只有在其他线程尝试竞争的时候才会释放,线程不会主动释放偏向锁
  一个同步方法被一个线程抢占到时,mark word里面会把锁最后三个标志位改为101,同时还会占用前54位记录当前线程id,若该线程再次访问时,只需判断偏向锁是否指向自己,不再需要去Monitor去竞争
偏向锁在无多线程竞争的情况下可以减少不必须要的轻量级锁执行路径,相比非同步的方法只有纳米的差距

偏向锁的获取:

当对象锁第一次被获取时,在Mark Word中记录下该线程的ID,这个线程称为偏向线程,锁就是偏向锁

在这里插入图片描述


在linux在中输入下面的命令查看偏向锁信息
在这里插入图片描述

最后一个true表示偏向锁是开启的,java6后默认开启,-XX:-UseBiasedLock,关闭轻量级锁,直接到重量级锁
4000表示启动有延迟4秒, -XX:BiasedLockingStartupDelay=0 ,延迟时间设置为0


测试

public static void main(String[] args) {
    Object object = new Object();
    synchronized (object) {
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

加入jvm启动参数, -XX:BiasedLockingStartupDelay=0, 把偏向锁的延迟改为0
可以看到第一个8位(每8位是倒序的)的后三位是101,表示是偏向锁

java.lang.Object object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
0     4        (object header)                           05 e8 0e 02 (00000101 11101000 00001110 00000010) (34531333)
4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8     4        (object header)                           28 0f ff 7f (00101000 00001111 11111111 01111111) (2147421992)
12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

如果不改延迟为0,直接轻量级锁,显示如下,第一个八位的最后两位是00

java.lang.Object object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
0     4        (object header)                           68 f2 43 02 (01101000 11110010 01000011 00000010) (38007400)
4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8     4        (object header)                           28 0f ff 7f (00101000 00001111 11111111 01111111) (2147421992)
12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

如果把偏向锁的延迟时间设置为0(或4秒后再进行锁竞争,因为默认偏向锁是4秒开启), 哪怕没有synchronic也直接是偏向锁,记录线程id的位置为空
-XX:BiasedLockingStartupDelay=0

public static void main(String[] args) {
    Object object = new Object();
    System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
java.lang.Object object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8     4        (object header)                           28 0f ff 7f (00101000 00001111 11111111 01111111) (2147421992)
12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

关闭偏向锁
-XX:-UseBiasedLocking 开启偏向锁 -XX:+UseBiasedLocking


01 轻量级锁

一旦有不同的线程获取或竞争锁对象,使用cas更新对象头失败后
先判断拥有偏向锁的线程是否还在执行,如果不执行,就把偏向锁改为无锁,再cas偏向于当前线程,锁不升级
若之前线程在运行,到了全局安全点(没有字节码执行),把之前线程暂停(stw),把锁升级为轻量级锁,仍然是之前线程持有,继续执行同步代码,其他线程cas自旋获取轻量级锁

Mrak Word存储的是指向线程栈中Lock Record的指针

目的:在大多数情况下同步块并不会出现竞争情况,都是不同线程交替持有锁,引入轻量级锁可以减少重量级锁对线程的阻塞带来的开销。
轻量级锁认为环境中线程几乎没有对锁对象的竞争,即使有竞争也只需要稍微等待(自旋)下就可以获取锁,但是自旋次数有限制,如果超过该次数,则会升级为重量级锁。

自旋锁
当锁被占用时,当前想要获取锁的线程不会被立即挂起,而是做几个空循环,看持有锁的线程是否会很快释放锁,在经过若干次循环后,如果得到锁,就顺利进入临界区;如果还不能获得锁,那就会将线程在操作系统层面挂起阻塞

自适应自旋
自旋的次数不是固定的,它由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

自旋和阻塞的区别
是不是放弃处理器的执行时间,阻塞放弃了CPU时间,进入等待区,等待被唤醒。响应慢。自旋锁一直占用CPU时间,时刻检查共享资源是否可以被访问,所以响应速度更快。

优点
如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好。
缺点
若持有锁的线程占用锁时间较长,等待锁的线程自旋一定次数后还是拿不到锁而被阻塞,那么自旋就白白浪费了CPU的资源
自旋的次数直接决定了自旋锁的性能。JDK自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。

jdk15后开始废弃偏向锁,因为开销比较大,默认已经不开启了,要开始的话要使用jvm参数


重量级锁

监视器锁Monitor
Mark Word存储的是指向堆中的Monitor对象的指针
synchronized监视器锁在互斥同步上对性能的影响很大。Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,状态转换需要花费很多的处理器时间,大大消耗性能,这种锁机制也被称为重量级锁

在轻量级锁中,如果cas自旋一直都没有获取的锁,就会消耗很大的cup资源,需要升级为重量级锁
在jdk6前是自旋了10次会升级为重量锁或自旋的线程数超过了cup核数的一半
在jdk6后是自适应自旋锁,如果线程自旋成功了,下次自旋的次数会增加,jvm认为既然上次都成功了那下次大概率也会成功, 如果线程很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免cup空转

有大量的线程争夺锁

 没有获取锁被阻塞的线程会被加入到阻塞队列中,等待被唤醒去争抢,是非公平公平锁
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值