一、概述
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表示的内容如下表
如上图所示,根据锁标志位就可以判断当前对象处于什么状态。
锁升级
JDK1.6中,为了减少加锁和释放锁的消耗,采用了多钟锁结构,其状态分别为无锁、偏向锁、轻量级锁、重量级锁。每个阶段的锁的资源消耗量是逐级递增的。这几个状态会随着竞争逐渐升级,这里要注意,这些锁不能降级
。
为什么不能降级,这是因为锁升级和降级是有资源消耗的,频繁的升降级锁,资源的消耗会更严重,这样就得不偿失。如果偏向锁升级成轻量级锁后没有再次触发升级,说明该资源竞争不激烈,轻量级锁也够用。如果持续升级到重量级锁,说明竞争非常激烈,此时要对各个等待的线程做相应的处理,比如线程的挂起和唤醒。
偏向锁的获取和释放
偏向锁是消耗资源最少的,主要是通过CAS将mark word的线程ID
指向当前线程,以此来获取锁。
整体过程如下:
- 线程通过CAS获取锁,成功进入步骤2,失败进入步骤3
- 线程获取成功,标志偏向锁
01
,,执行同步代码块,进入步骤5- 线程获取失败,说明已有线程占用,此时进行
锁升级
,进入步骤4- 在JVM到达全局安全点(这是JVM决定的,一般将循环的末尾、方法返回前等作为安全点),
获得偏向锁的线程被挂起,撤销偏向锁,并升级锁,锁标志位
变为00
,完成之后获得锁的线程继续执行,未获得锁的线程进行自旋,尝试获取锁(这时是轻量级锁了),进入步骤6- 如果锁没有升级,则通过CAS的方式将mark word中
线程ID
清除即可- 如果升级成了轻量级锁,那么请看
轻量级锁的撤销
步骤
偏向锁的获取和释放:通过CAS方式修改mark word中的线程
偷一张《Java并发编程的艺术》的图
轻量级锁的获取和释放
轻量级锁的出现说明有多个线程在竞争了,此时未获得锁的线程将进入自旋状态,如果自旋到一定程度,锁将会升级。毕竟线程自旋是要消耗CPU的,升级锁之后线程会被挂起,等待唤醒,这样就可以减少资源的消耗。
轻量级锁的获取和释放过程如下
- 线程通过CAS的方式将mark word的记录指针指向当前线程,成功则进入步骤2
- 获取成功,执行同步代码块,结束后进入步骤5
- 获取失败,进行自旋,并一直尝试通过CAS的方式修改 mark work,如果成功则进入步骤2,如果失败到一定次数,进入步骤4
- 多次自旋失败,进行锁膨胀,膨胀完成之后获得锁的线程继续执行代码,未获得锁的线程被挂起,等待被唤醒。
- 代码运行结束,通过CAS替换 mark word来释放锁,如果锁进行膨胀,此时看
重量级锁的撤销
偷一张《Java并发编程的艺术》的图
重量级锁的获取和释放
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
重量级锁获取和释放过程如下:
- 通过CAS将monitor的 owner 设置为当前线程
- 如果 owner 为当前线程,表示重入锁,记录重入的次数
- 如果锁获取失败,线程会被挂起,并且进入等待队列
- 持有锁的线程执行完毕,通过CAS方式将 owner 清除,然后取出等待队列的线程,将其唤醒
本文讲了锁升级的大致过程,但是没讲太深入,有需要的可以看看这篇文章《JVM源码分析之synchronized实现》,看看源码是怎么实现的。