synchronized的底层实现

引言

在单机环境中的并发编程中,需要用锁来保证数据的安全性。我们经常会用到synchronized,那么JVM中是如何实现synchronized的呢,在这篇文章中,我会从锁分类和锁膨胀(锁升级)的角度,会来探析一二。

为什么要有锁对象

Object lockObj = new Object();
synchronized(lockObj){
    //TODO
}

在访问某块代码或者变量时,为了防止线程安全问题,我们需要先获取锁对象,通过锁的互斥来保证线程的安全访问。在上面这块代码中,lockObj就是锁对象。

synchronized底层实现的锁分类

  1. 偏向锁
  2. 轻量级锁
  3. 重量级锁
    在这3种分类中,偏向锁和轻量级锁是在逻辑层面做的一些处理,并非真正的锁,只有重量级锁,才是真正的锁(操作系统层面的锁)。

为什么要对锁分类

假设现在有A和B两个线程,要访问共有变量。一般来说有如下3种情况:

  1. 每次只有线程A或者线程B单独使用。
  2. 线程A和线程B交替使用。
  3. 线程A和线程B,同时使用。

众所周知,获取锁是比较消耗性能的。所以在Java中,synchronized提供了几种锁的实现来优化。
对于这3种情况,前两种可以在逻辑层面做一些处理,避免每次获取锁(操作系统锁),带来的性能开销。对于1,可以使用偏向锁。对于2,可以使用轻量级锁。

偏向锁

偏向锁会保证对象被线程安全的访问。

锁对象

被synchronized锁保护的,称作锁对象。锁对象中包含了锁对象头,由线程idEpoch、分代年龄、是否偏向锁标记、锁标记组成。

线程id:每次获取锁对象时,会先检查线程id是否与当前线程一致,如果线程id是空,则通过CAS设置对象头中的线程id。
Epoch:本质是时间戳。使用Epoch通过CAS来保证设置线程id的安全性。

运行原理

在获取锁对象时,首先会检查锁对象头中的线程id是否与当前线程一致。

  1. 如果线程id是空,则通过CAS设置对象头中的线程id,并更新Epoch。
  2. 如果线程id与当前线程一致,则可以安全访问。
  3. 如果线程id与当前线程不一致,则需要锁膨胀。(升级为轻量级锁)

轻量级锁

在偏向锁获取不到锁对象时,会通过自旋来不断的尝试获取锁,这就称为轻量级锁。

重量级锁

在通过一定的自旋次数后,如果还获取不到锁,就会升级为重量级锁,所有获取不到锁对象的线程都会被阻塞(Blocked状态)。
重量级锁会使用Monitor获取操作系统的MutexLock(互斥锁)

什么时候切换锁类型

在默认情况下,会先尝试使用偏向锁,如果获取不到,则升级为轻量级锁,轻量级锁在一定的自旋次数后,会升级为重量级锁。获取锁是按:偏向锁->轻量级锁->重量级锁,依次升级,且无法降级。
只有当当前锁无法获取到锁对象时,才会升级。

posted @ 2021-12-14 16:05  划破黑夜  阅读(163)  评论(0编辑  收藏  举报