Synchronized优化探究

332 阅读3分钟

最近正在看《并发编程的艺术》这本书,因为之前也阅读了大量关于多线程的博客,所以读起来还是很流畅的,基本没有遇到什么问题。但是就书中Synchronized优化这一部分产生了小小的疑虑。偏向锁何时膨胀为轻量级锁,轻量级锁何时又膨胀为重量级锁,因此就产生了这篇文章。 Synchronized是这篇文章的主角。相对于保证可见性的关键字volatiley以及各种并发安全的concurrent容器,隐式锁Synchronized确实显得有些重了。但是有很多复杂的多线程并发情况确实需要锁来保证并发安全,因此Java1.6里对Synchronized进行了优化。在只有单线程重复执行同步代码(使用偏向锁),以及多线程无争抢轮流执行同步代码(使用轻量级锁)两种场景下极大地提升Synchronized的性能。对于多线程争抢这一场景,Synchronized依旧使用重量级锁,没有变化。

锁的膨胀

java中锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

《并发编程的艺术》中的对象头说明

偏向锁的膨胀

锁是存在Java对象头里的。在最开始一个单线程通过同步代码块的时候,会给Synchronized的对象头里加上偏向锁,并将自己的线程ID通过CAS存储在对象头中。当线程自己再次通过时,只需要校验一下线程ID就可以重入,效率很高,这也就是偏向锁优化的单线程场景。这时假如又来了一个需要通过同步块的线程,检查对象头中的线程ID不是自己这个线程。这时会去找原来持有这个偏向锁的线程是否还是活动线程,如果已经不活动则将偏向锁的标识置为0,线程2重做上面的步骤取得偏向锁。如果原来持有偏向锁的线程还是活动线程,则暂停原有的持锁线程,此时将偏向锁升级为轻量级锁。首先将对象头的锁标志位改为00,之后在持有偏向锁的线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。这就完成了锁的膨胀,之后恢复持有轻量级锁的线程。

轻量级锁的膨胀

线程解锁时使用CAS将对象头中的指针替换回Mark Word。新来的线程在持锁时使用CAS将对象头中的Mark Word替换为指向自己锁记录的指针。这也就是轻量级锁优化的多线程交替执行同步代码的场景。当以上两种CAS操作失败时,轻量级锁膨胀为重量级锁,新入的线程自旋等待锁,自旋一段时间还无法取得锁时线程堵塞。这与《并发编程的艺术》中描述的可能有些许不同,书中描述自旋一定时间还是持锁失败后轻量级锁才膨胀为重量级锁。而自旋这个动作本身就是JVM针对重量级锁的优化,所以只要CAS失败产生竞争后,轻量级锁就进行了膨胀。