Synchronized锁升级过程详解

一、概述

synchronized是Java中经常用到的同步锁,在以前,它是一个十分笨重的,每次加锁、释放锁都要进行内核态和用户态的切换,十分耗费资源。在JDK1.6以后,synchronized内部得到了巨大的优化,引入了偏向锁和轻量级锁,它也变得轻便起来。为什么引入其他的锁机制,这是因为程序在执行同步代码块中,绝大部分都没有发生竞争关系,即使如此,synchronized还是会从用户态转到内核态,耗费了大量不必要的时间。

锁的标志和存储结构

synchronized使用方式

synchronized必须作用在某个对象或者类中,如下代码

// 作用于静态方法,锁住的是整个类
public synchronized void func( ){
}

// 作用于普通方法,锁住的是该方法的实例类
public synchronized void func( ){
}

//作用于代码块,锁住的是括号内的对象
public void func( ){
    synchronized(obj){
    }
}

Java对象有关存放锁的结构

可以看到synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。

java对象都是二进制形式的,在32位的机子上,头文件的其中32个bit就是Mark Word,用于表示对象的状态,每个bit表示的内容如下表
20200326-1
如上图所示,根据锁标志位就可以判断当前对象处于什么状态。

锁升级

JDK1.6中,为了减少加锁和释放锁的消耗,采用了多钟锁结构,其状态分别为无锁、偏向锁、轻量级锁、重量级锁。每个阶段的锁的资源消耗量是逐级递增的。这几个状态会随着竞争逐渐升级,这里要注意,这些锁不能降级

为什么不能降级,这是因为锁升级和降级是有资源消耗的,频繁的升降级锁,资源的消耗会更严重,这样就得不偿失。如果偏向锁升级成轻量级锁后没有再次触发升级,说明该资源竞争不激烈,轻量级锁也够用。如果持续升级到重量级锁,说明竞争非常激烈,此时要对各个等待的线程做相应的处理,比如线程的挂起和唤醒。

偏向锁的获取和释放

偏向锁是消耗资源最少的,主要是通过CAS将mark word的线程ID指向当前线程,以此来获取锁。

整体过程如下:

  1. 线程通过CAS获取锁,成功进入步骤2,失败进入步骤3
  2. 线程获取成功,标志偏向锁01,,执行同步代码块,进入步骤5
  3. 线程获取失败,说明已有线程占用,此时进行锁升级,进入步骤4
  4. 在JVM到达全局安全点(这是JVM决定的,一般将循环的末尾、方法返回前等作为安全点),
    获得偏向锁的线程被挂起,撤销偏向锁,并升级锁,锁标志位变为00,完成之后获得锁的线程继续执行,未获得锁的线程进行自旋,尝试获取锁(这时是轻量级锁了),进入步骤6
  5. 如果锁没有升级,则通过CAS的方式将mark word中线程ID清除即可
  6. 如果升级成了轻量级锁,那么请看轻量级锁的撤销步骤

偏向锁的获取和释放通过CAS方式修改mark word中的线程

偷一张《Java并发编程的艺术》的图

20200326-2

轻量级锁的获取和释放

轻量级锁的出现说明有多个线程在竞争了,此时未获得锁的线程将进入自旋状态,如果自旋到一定程度,锁将会升级。毕竟线程自旋是要消耗CPU的,升级锁之后线程会被挂起,等待唤醒,这样就可以减少资源的消耗。

轻量级锁的获取和释放过程如下

  1. 线程通过CAS的方式将mark word的记录指针指向当前线程,成功则进入步骤2
  2. 获取成功,执行同步代码块,结束后进入步骤5
  3. 获取失败,进行自旋,并一直尝试通过CAS的方式修改 mark work,如果成功则进入步骤2,如果失败到一定次数,进入步骤4
  4. 多次自旋失败,进行锁膨胀,膨胀完成之后获得锁的线程继续执行代码,未获得锁的线程被挂起,等待被唤醒。
  5. 代码运行结束,通过CAS替换 mark word来释放锁,如果锁进行膨胀,此时看重量级锁的撤销

偷一张《Java并发编程的艺术》的图
20200326-3

重量级锁的获取和释放

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

重量级锁获取和释放过程如下:

  1. 通过CAS将monitor的 owner 设置为当前线程
  2. 如果 owner 为当前线程,表示重入锁,记录重入的次数
  3. 如果锁获取失败,线程会被挂起,并且进入等待队列
  4. 持有锁的线程执行完毕,通过CAS方式将 owner 清除,然后取出等待队列的线程,将其唤醒

本文讲了锁升级的大致过程,但是没讲太深入,有需要的可以看看这篇文章《JVM源码分析之synchronized实现》,看看源码是怎么实现的。