0
点赞
收藏
分享

微信扫一扫

这次终于明白synchronized锁优化了!


本文参考了《深入理解java虚拟机》锁优化章节,尚硅谷JUC,黑马多线程视频,再增加了一些自己的理解,绝不是胡诌的哦。

文章目录

  • ​​1. 自旋锁是什么?​​
  • ​​2. 锁消除​​
  • ​​3. 轻量级锁​​
  • ​​3.1 对象头​​
  • ​​3.2 轻量级锁的加锁过程​​
  • ​​3.3 轻量级锁的解锁过程​​
  • ​​4. 重量级锁​​
  • ​​5. 偏向锁​​
  • ​​6. 补充:关于HashCode​​

1. 自旋锁是什么?

这其实是在jdk4引入的手段,一般来讲加锁解锁会涉及到cpu从内核态到用户态之间的切换,而如果某个线程持有锁的时间很短,那么我们只需要稍微让cpu空转一下,即一个执行一小段时间的while循环,结束后发现线程可以获取锁了,那么就减去了切换的开销。

这确实是个好主意,并且现在是多核时代,如果对于单核cpu,那么cpu空转会白白浪费性能,但是多核cpu,一两个cpu空转等待这个开销是值得的,在jdk6以后,自旋锁升级为了自适应自旋锁,jvm会自己去判断该不该进行自旋,这方面其实都不太需要我们考虑了。

2. 锁消除

这里用了深入理解java虚拟机中的例子。

在jdk5中下面代码会被转化成下面的代码。

这次终于明白synchronized锁优化了!_jvm


这次终于明白synchronized锁优化了!_java_02


我们知道StringBuffer是线程安全的集合,本身append方法就是使用​​synchronized​​修饰的,这就造成了一个方法里有多个锁块,而程序其实会对其进行优化,将局部方法中的同步代码块进行消除,以提高程序的执行效率。

3. 轻量级锁

它并不是用来代替重量级锁的,而是在没有锁竞争的情况下,减少重量级锁使用操作系统互斥量带来的性能开销的。

所以关键在于,如果锁竞争比较激烈,轻量级锁反而会有很大的开销,核心就在于它内部的自旋,jvm做的聪明的一点在于,如果处于竞争激烈的情况下,那么就会升级为重量级锁。

那我们详细来讲讲吧,首先我们需要一些前置知识。

3.1 对象头

复习下对象头吧,对象头主要有两部分,对象标记和类元信息。

对象标记指的就是Mark Word,有以下内容,其中标志位和状态,指针都比较重要。

这次终于明白synchronized锁优化了!_开发语言_03


而类元信息存储的是方法区对象类型数据的指针。补充下这张图。

这次终于明白synchronized锁优化了!_加锁_04

3.2 轻量级锁的加锁过程

首先一个对象没有被锁定时,它的锁标志位是01,当代码快要进入同步代码块时,会在线程栈帧中新建了一个名为​​Lock Record​​​ 锁记录的空间,用来存储​​Mark Word​​​的拷贝,官方把这玩意称之为​​Disokaced Mark Word​​,如下所示。

这次终于明白synchronized锁优化了!_开发语言_05


此时虚拟机将使用CAS将对象中的​​Mark Word​​更新为锁记录的指针,如果更新成功,就代表线程拥有了这个对象的锁,

这次终于明白synchronized锁优化了!_加锁_06


下面是更新成功的情况,对象头的​​Mark Word​​的锁标志位也同时被更新为00了。

这次终于明白synchronized锁优化了!_自旋锁_07


但上面只是介绍了从无锁->轻量级锁的过程。

而如果对象已经持有锁了呢?

那么就分为两种情况,对象Mark Word是否指向当前的线程栈帧?

如果是,说明是一个线程多次进入同一把锁,也就是重入的情况,会再添加一条锁记录作为重入的技术,而后直接进入执行代码即可。

这次终于明白synchronized锁优化了!_jvm_08

如果不是,说明存在竞争情况,如下所示,线程0已经加了轻量级锁,而线程1想要再加,CAS失败,进入锁膨胀的情况。

这次终于明白synchronized锁优化了!_开发语言_09


Object对象申请重量级锁Monitor,这个是操作系统的锁,而后Mark Word指向了Monitor的地址,同时线程1进入EntryList阻塞等待线程0释放。

这次终于明白synchronized锁优化了!_开发语言_10

3.3 轻量级锁的解锁过程

解锁同样是通过CAS操作,也分为几种情况。

1.重入解锁成功,如果有取值为null的记录,表示有重入的情况,去掉该锁记录,并将重入计数-1。

这次终于明白synchronized锁优化了!_jvm_11

2.正常解锁成功,没有竞争情况,且锁记录没有为null的记录,此时会通过CAS将Mark Word中的记录更新,对象头重置为无锁00状态。

这次终于明白synchronized锁优化了!_加锁_12


3.解锁失败,此时表明存在竞争,膨胀为重量级锁。还是这张图,本想将Object中Mark Word的内容还原,但CAS失败,因为内容已经变成monitor的地址了此时要将owner置为null 唤醒在EntryList中的线程1。

这次终于明白synchronized锁优化了!_jvm_13

4. 重量级锁

如果线程2进入了同步代码块,那么就进入的java对象还会关联一个Monitor对象。

这次终于明白synchronized锁优化了!_java_14

其中Owner指向了线程2,而其他竞争的线程345则进入EntryList中等待,其中EntryList是一个双向链表,并且345之间存在竞争,是非公平的。

这次终于明白synchronized锁优化了!_开发语言_15


再来说说图重点WAITING。

线程01判断条件是否满足,不满足调用wait方法进入waiting队列中等待,当线程2释放锁 owner为null时,blocked队列就开始竞争锁了,而释放锁时如果调用了notify/notifyAll方法 就会将单个或所有线程都放入到blocked队列中竞争锁。

5. 偏向锁

首先要明白偏向锁的几个特性。

为什么要引入偏向锁?

为了弥补轻量级锁多次cas比较带来的性能损耗,偏向锁是完全无竞争,连CAS操作都省略掉了,很多情况下只有一个线程反复进入同步代码快时,就很有用了。

而一旦发生了竞争情况,偏向锁就会升级,这也是在所难免的,值得一提的是,在jdk15后就废弃了偏向锁,但我们现在还是用的上的。

什么时候会升级为偏向锁?

其实默认会在4秒后开启偏向锁看,所以一般情况下4秒钟内的对象默认是轻量级锁,有两个办法可以改变。

1.延迟参数设置为0,即​​-XX:BiasedLockingStartupDelay=0​

2.延迟4秒后 再创建对象

我们一般讲的是将延迟参数设置为0的情况。

偏向锁的加锁很简单,在无锁的情况下,会将对象头中的偏向模式设置为1,标志位变为01,简称101,同时使用CAS操作将该线程的ID记录到对象头中。

这次终于明白synchronized锁优化了!_自旋锁_16

有锁的情况下,只需要判断对象头中的线程ID是否一致即可,一致无需任何其他操作,直接往下执行。

有一种批量重偏向的情况,有以下情况,线程1循环进入同步代码快,此时对象头的锁id为线程1的id,过了一会线程2循环记录同步代码块,但这是没有竞争的情况下,所以不会一下子的改变对象头的id。

可以看下下面代码,t1线程先是30次加锁,每个对象放入集合,每个对象的mark word都为当前线程的id。

t2线程加锁获取对象,打印对象内容。

这次终于明白synchronized锁优化了!_自旋锁_17


这里截取部分内容,两张图可以对比一下,发现在t2线程第20次循环时,对象的记录结果和第19次的三次打印是不同的,即第20次第一段代码打印的是指向线程1的对象头,第二次加锁后就变成了指向线程2的对象头了,后面的结果也是一样。

这次终于明白synchronized锁优化了!_加锁_18

这次终于明白synchronized锁优化了!_加锁_19


得出结论,当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至

加锁线程。

不一致的情况,说明存在竞争了,此时需要撤销偏向锁。

撤销偏向锁有以下情况。

1.当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁,竞争会导致锁升级。
2.使用hashcode时,会了保证hashcode的一致性,会直接膨胀为重量级锁
3.在代码中使用到了wait/notify时,也会膨胀为重量级锁
4.批量撤销的情况,比如偏向锁t1线程占用 后面改为t2占用,等撤销达到第40次后,jvm发现撤销次数太多了,可能就不该偏向 ,所以就算新创建的对象都是无锁的,进锁块直接就是轻量级锁,不会有偏向锁了。(这种情况有空补下代码吧)

6. 补充:关于HashCode

下面总结了几种情况,在调用hashcode时,不同加锁情况下的变化。

这次终于明白synchronized锁优化了!_加锁_20


举报

相关推荐

0 条评论