Java并发——synchronized关键字性能分析

前言

  所有Java开发者对于synchronized关键字都不陌生。
其实对于synchronized关键字的理解莫过于这几个方面:对象在内存中的布局、字节码层面的执行指令—monitorenter-monitorexit、ObjectMonitor(C++语言)、锁膨胀等。当然前提是要对于锁实现的方式、锁的优化等有清晰认知。关于synchronized原理介绍网络上有很多精辟的总结,我们就不重复造轮子了。在这里我贴出我认为不错的文章:

synchronized底层实现

我想重点说的是关于锁膨胀过程中的性能探讨

  通常情况下,锁膨胀的方向:无锁->偏向锁->轻量级锁->重量级锁。

偏向锁

  当第一次有线程来获取锁时,,会把对象头中的标志位更新为:01,把偏向模式设置为1,同时会通过CAS操作,将当前线程ID记录在Mark Word中表示进入到偏向锁模式。如果CAS操作成功,那么持有偏向锁的线程每次需要进入到与这个锁相关的同步代码块都不需要执行任何同步操作。这便是偏向锁的意义:消除同步原语!
如果一旦有其他线程去尝试获取这个锁,锁偏向模式立马宣告结束。此时会根据锁被持有的状态决定是否撤销偏向(偏向模式设置为0),偏向模式撤销后对象头中的标志位有以下两个改变的方向。如果对象未被锁定,则恢复为无锁模式(标志位01),如果对象被锁定,则标志位变成00,表示锁进入到轻量级锁模式,后续的同步操作将按照轻量级锁模式执行。

轻量级锁

在代码即将进入到同步代码块时,如果此时是无锁状态(标志位01,但此时偏向锁模式已经失效)首先会在当前线程的栈桢是拷贝一份Mark Word,称为锁记录(Lock Record),然后通过CAS操作尝试将Mark Word更新为执行Lock Record的指针。如果更新成功,则说明获取到锁成功,此时标志位会更新为00状态。
但是,如果CAS更新失败了,表示此时至少有一个线程在参与锁的竞争,此时虚拟机会优先检查Mark Word是否指向当前线程的栈桢,如果是,说明当前线程已经获取到锁,继续执行同步代码块就可以。如果不是,说明这个锁已经被其他线程抢占了。如果出现了多个线程同时在参与锁的竞争,那么此时轻量级锁已经无效,必须膨胀为重量级锁,将锁标志更新为10. 此时Mark Word指向重量级锁(互斥量)的指针。

我们从轻量级锁的工作原理可以看出,轻量级锁通过CAS操作“轻量级”的获取锁,因此提升了性能。但是如果突然有两个以上的线程参与锁的竞争,轻量级锁的CAS操作必然失败,然后升级为重量级锁,也就是说在这种情况下,CAS操作是无用的,反而成为了消耗性能的行为,比重量级锁“更重”。
轻量级模式下,不同的线程在不同的时间获取锁,也就是每个线程获取锁的CAS操作都是成功的。一旦出现了不同的线程在同一时间竞争锁,则会膨胀为重量级锁模式。

CAS操作失败之后是否会立即升级为重量级锁这个问题,我目前依然没有找到答案~
个人猜测应该是受到JVM自旋配置控制。如果JVM开启了自适应自旋配置,一定次数的CAS操作之后依然没有竞争到锁就会升级为重量级锁,如果没有开启自适应自旋配置,那么应该会立即升级为重量级锁。在JDK6之后,JVM的自适应自旋配置是默认开启的~

重量级锁

重量级锁的核心就是ObjectMonitor.此对象中有几个非常重要的属性:

  1. 计数器:支持重入;
  2. owner:锁的拥有者,没有竞争时,为null;
  3. 等待池:线程获取锁的情况下,主动wait释放锁资源,将会进入到等待池。等待被唤醒或者主动醒来,再次参与锁的竞争;
  4. 锁池:在参与锁竞争的过程中竞争失败会进入到锁池中,等待锁资源释放再次参与锁的竞争。

重量级锁的重体现在两个方面:

  1. 互斥同步导致的线程调度所引起的用户态与内核态的切换而带来的性能消耗;
  2. 多线程同步竞争锁,参与竞争的线程越多,性能损耗越严重。

再谈锁膨胀

锁的膨胀方向一定是严格按照:无锁->偏向锁->轻量级锁->重量级锁吗?非也!
对象头的设计十分精妙,为了尽可能的减少内存的消耗,对象头被设计成一个可以在不同标志位下存储不同的数据。正因如此,导致锁的膨胀方向不可逆
对象中还可以存放对象的hashCode,只有在程序是显式的调用了Object::hashCode()方法后才会真正的计算出hashCode并进行存储。
在无锁的情况下,假如我们没有显式的计算hashCode,此时的状态是无锁可偏向的,这种情况下,锁的偏向可以为:
无锁->偏向锁->轻量级锁->重量级锁

当显式的计算了hashCode后,计算后的值将会被保存在对象头中此时将无法到偏向锁模式,原因就在于无法记录线程ID了,此时的状态是无锁不可偏向,这种情况下,锁的偏向可以为:无锁->轻量级锁->重量级锁
现在,假设我们在偏向锁模式下,显式的计算了hashCode,此时会立即撤销偏向锁模式直接膨胀为重量级锁,这种情况下,锁的偏向为:无锁->偏向锁->重量级锁。
在重量级锁的实现中,对象头指向了重量级锁的位置,在重量级锁的实现中,ObjectMonitor中有字段可以记录非加锁状态下的Mark Word,自然也就可以保存hashCode值。

你可能感兴趣的:(并发编程,java,多线程,面试)