多线程并发 之 synchronized 锁的优化

为了提高阅读的体验,可以点击这里

前言

早期版本的synchronized在性能上比较差,好在Jdk1.6之后对其进行种种优化,那么这篇我们就来学习一下synchronized锁都有哪些优化操作!因为网上关于这块的解析比较多了,所以基础如自旋、Mark Word就不再复述了,主要讲我对锁优化的认识!

重量锁

我觉得要想了解 synchronized 的优化,就必须要先认识到早期 synchronized 中的传统锁有哪些不足点,这里的传统锁就是经常听到的重量锁。那么先来看一下重量锁是如何工作的。

在Java中每个对象都有一个monitor对象与之对应,在重量级锁的状态下,对象的mark word存放的是一个指针,指向了与之对应的monitor对象。这个monitor对象就是实现重量锁的关键。注意我这里说的是实现重量锁的关键,所以偏向锁、轻量锁在实现上和monitor是没有关系的

一个monitor对象包括这么几个关键字段:ContentionList,EntryList ,WaitSet,owner。其中ContentionList、EntryList 、WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。

◆ Contention List:所有请求锁的线程将被首先放置到该竞争队列
◆ Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
◆ Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set。
◆ OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
◆ Owner:获得锁的线程称为Owner。
◆ !Owner:释放锁的线程。

在这里插入图片描述

重量锁竞争的过程:

EntryList与ContentionList逻辑上同属等待队列ContentionList会被线程并发访问,为了降低对ContentionList队尾的争用,而建立EntryList。

当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到ContentionList的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将ContentionList中的所有元素移动到EntryList中去,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。

重量锁性能差的原因:

以上就是竞争重量锁的基本情况,我们可以知道实现的关键在于monitor。而monitor是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。

重量锁更适合多线程同时进入临界区

轻量锁

JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。

在重量锁的时候Mark Word指向了与之对应的monitor对象,在轻量锁的情况下Mark Word也是存放了一个指针,这个指针指向了竞争线程帧栈中的锁记录。听起来很绕口,看下面竞争的过程:

在这里插入图片描述

轻量锁竞争:

当线程试图获取一个锁时,会先在当前的帧栈中创建用于存储锁记录的空间,然后将Mark Word的内容拷贝到这个记录空间。

接着线程会再试图通过CAS修改Mark Word,让Mark Word中的内容变成一个指针,指向刚刚创建的锁记录的地址。修改成功的线程就拿到了锁。如果失败了,它不会像重量锁一样马上将线程挂起,轻量锁就是要解决重量锁动不动就把线程挂起来的操作(内核态与用户态的切换成本高),因为它认为锁马上就会被释放掉,它会通过占用CPU再去尝试修改几次,这个过程就是自旋轻量锁优化的地方在于:它认为自旋的时间与线程挂起的操作相比是划算的

轻量锁释放:

如果一个线程2通过自旋还是没有得到锁,它就认为再这样占用CPU的时间反而不值当了,还不如把线程挂起来,不去浪费CPU的时间,等待占用锁的线程1释放之后的中断。也就是说这个锁已经不适合使用轻量锁了,应该膨胀为重量锁,于是自旋失败的线程2会把Mark Word修改成指向monitor的指针

线程1同步块执行完毕之后发现,诶!Mark Word原来不是指向线程1中的锁记录吗?!!怎么现在指向其他地方了(指向monitor),他知道了有其他线程因为自旋几次失败,然后修改了Mark Word。那么线程1释放完锁后就唤醒被挂起的线程2。然后这个锁就变成了重量锁。

线程1同步块执行完毕,如果发现Mark Word还是指向当前线程的锁记录那么说明没有其他线程在它使用的期间因为获取锁失败,而修改Mark Word的值。于是把刚刚拷贝到帧栈中的Display Mark Word拷贝回原来的对象头中。竞争不够强烈,还是使用轻量锁。

轻量锁更适用于多个线程交替进入临界区

偏向锁

因为轻量锁想要知道多线程竞争是否激烈(究竟是交替进入还是同时竞争),所以会拷贝Mark Word以及通过CAS修改Mark Word,过程会涉及到多次CAS(CAS本身仍旧是一种操作系统同步原语,有一定的开销)。但JVM的开发者发现多数情况下:一个对象在一段很长的时间内都只被一个线程用。那么偏向锁就此出现。

从上面的轻量锁和重量锁我们知道一个对象的Mark Word会存放一个指针,而在偏向锁中,Mark Word存放线程ID来标识当前使用的线程

在这里插入图片描述

偏向锁加锁:

当该对象第一次被线程获得锁的时候,发现是匿名偏向状态,则会用CAS指令,将mark word中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。

当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。safe point这个词我们在GC中经常会提到,其代表了一个状态,在该状态下所有线程都是暂停的(大概这么个意思)

偏向锁为什么要升级:

我当时在想如果不存在重量、轻量锁,无论线程多少个都使用偏向锁来做可以吗?我觉得是可以的。但是你想如果当其他线程想要获取的时候,就需要等到safe point,而等待是不是就要通过挂起或者自旋呢?那么到底是使用自旋还是挂起呢?当然是先考虑自旋了。所以偏向锁就变成了轻量锁,轻量锁不适合再使用重量锁。所以不存在哪个锁代替哪个锁,只是哪个场景适用,就用哪个!

当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略(线程会在帧栈中复制一个Displaced Mark Word为空的Lock Record用来表示重复进入的一个次数,因为操纵的是线程私有的栈,因此不需要用到CAS指令,所以性能很高

锁优化总体流程图

当使用synchronized代码块的时候,通过反编译得到字节码后可以看到代码块上有对应的monitor:

monitorenter  //进入同步方法
//..........省略其他  
monitorexit   //退出同步方法
//省略其他.......
monitorexit //退出同步方法

synchronized代码块的时候,可以看到其字节码会将该方法标识位ACC_SYNCHRONIZED,指明位同步方法:

public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    //省略其他.......

当线程访问同步快时,就会先判断当前锁状态,然后做出对应的操作:

在这里插入图片描述

我之前的疑惑

  1. 当时在想synchronized针对代码块、方法块的时候,不是没有指定对象吗?没有对象不就没有Mark Word吗?现在再去看就知道为什么synchronized针对代码块、方法块是以当前对象的为锁的,这样就解决Mark Word不存在的疑惑了。
  2. 潜意识认为synchronize性能很差,但这个只是针对早期的版本,因为早期的版本只有重量锁,所以性能差。

参考文章

http://blog.sina.com.cn/s/blog_c038e9930102v2i0.html

https://github.com/farmerjohngit/myblog/issues/12

https://blog.csdn.net/javazejian/article/details/72828483#偏向锁

https://www.zhihu.com/question/53826114/answer/236363126

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值