首先说明一下,锁升级和锁降级说的根本不是一个事情,锁升级是synchronized关键字在jdk1.6之后做的优化,锁降级是为了保证数据的可见性在添加了写锁后再添加一道读锁,锁降级请参考链接1。本文主要针对锁升级介绍。
一、锁升级
之前介绍过synchronized关键字,synchronized关键字可以锁类,锁方法和锁代码块,有关synchronized关键字的使用可以参考链接2,synchronized锁一致被认为是比较重量级的锁,但JDK1.6之后对synchronized锁是有优化的,本文详细介绍一下JDK1.6之后synchronized锁的底层实现原理。
1、Java对象在堆内存的构成
在JVM中,对象在堆内存中分为三块区域:
(1)对象头
对象头相当于对象的元数据信息,对象头由两部分组成:
(a)Mark Word(标记字段)
存储对象的HashCode、分代年龄和锁标志位信息,在运行期间Mark Word里存储的数据结构会随着锁标志位的变化而变化,Mark Word的结构图如下,图摘自链接3:
上面提到了Mark Word被设计成一个非固定结构,在运行期间会随着锁标志位的变化而变化,上图中一个锁标志位所在的一行数据结构就对应一种Mark Word结构。
(b)Klass Pointer(类型指针)
对象指向它的类元数据的指针,JVM通过这个指针来确定对象是哪个类的实例。
(2)实例数据
这部分主要存放类的数据信息和父类信息。
(3)填充数据
JVM要求对象的起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
2、synchronized锁/重量级锁
这里主要介绍一下通常说的synchronized锁或者重量级锁的底层实现原理
2.1 Monitor对象
我们经常说synchronized关键字获得的是一个对象锁,那这个对象锁到底是什么?
每一个对象的对象头会关联一个Monitor对象,这个Monitor对象的实现底层是用C++写的,对应在虚拟机里的ObjectMonitor.hpp文件中。
Monitor对象由以下3部分组成:
(1)EntryList队列
当多个线程同时访问一个Monitor对象时,这些线程会先被放进EntryList队列,此时这些线程处于Blocked状态;
(2)Owner
当一个线程获取到了这个Monitor对象时,Owner会指向这个线程,当线程释放掉了Monitor对象时,Owner会置为null;
(3)WaitSet队列
当线程调用wait方法时,当前线程会释放对象锁,同时该线程进入WaitSet队列。
Monitor对象还有一个计数器count的概念,这个count是属于Monitor对象的,而不属于某个获得了Monitor对象的线程,当Monitor对象被某个线程获取时,++count,当Monitor对象被某个线程释放时,--count。
2.2 同步代码块和同步方法
synchronized关键字可以修饰方法,也可以修饰代码块,二者底层的实现稍有不同。
(1)同步代码块
public void method(){
synchronized(new Object()){
do something...
}
}
当进入method方法的synchronized代码块时,通过monitorenter指令获得Monitor对象的所有权,此时count+1,Monitor对象的owner指向当前线程;如果当前线程已经是Monitor对象的owner了,再次进入synchronized代码块时,会将count+1;当线程执行完synchronized代码块里的内容后,会执行monitorexit,对应的count-1,直到count为0时,才认为Monitor对象不再被线程占有,其他线程才可以尝试获取Monitor对象。
(2)同步方法
当线程调用到方法时,会判断一个标志位:ACC_SYNCHRONIZED。当方法是同步方法时,会有这个标志位,ACC_SYNCHRONIZED会去隐式调用那两个指令:monitorenter和monitorexit去获得和释放Monitor对象。
归根到底,synchronized关键字还是看哪个线程获得了对象对应的Monitor对象。
3、锁升级过程
JDK1.6之前,synchronized的实现涉及到操作系统实现线程之间的切换时需要从用户态切换为核心态,这是很消耗资源的,这也是早期synchronized锁称为“重量级”锁的原因,jdk1.6之后对synchronized锁进行了优化,引入了偏向锁和轻量级锁的概念,即synchronized锁有具体4种状态,这几个状态会随着竞争程度逐渐升级,就是锁升级。
3.1 synchronized锁的4种状态
synchronized锁有无锁、偏向锁、轻量级锁和重量级锁4种状态,在对象头的Mark Word里有展示,锁状态不同,Mark Word的结构也不同。
(1)无锁
很好理解,就是不存在竞争,线程没有获取synchronized锁的状态。
(2)偏向锁
即偏向第一个拿到锁的线程,锁会在对象头的Mark Word通过CAS(Compare And Swap)记录获得锁的线程id,同时将Mark Word里的锁状态置为偏向锁,是否为偏向锁的位也置为1,当下一次还是这个线程获取锁时就不需要通过CAS。
如果其他的线程尝试通过CAS获取锁(即想将对象头的Mark Word中的线程ID改成自己的)会获取失败,此时锁由偏向锁升级为轻量级锁。
(3)轻量级锁
JVM会给线程的栈帧中创建一个锁记录(Lock Record)的空间,将对象头的Mark Word拷贝到Lock Record中,并尝试通过CAS把原对象头的Mark Word中指向锁记录的指针指向当前线程中的锁记录,如果成功,表示线程拿到了锁。如果失败,则进行自旋(自旋锁),自旋超过一定次数时升级为重量级锁,这时该线程会被内核挂起。
(4)自旋锁
轻量级锁升级为重量级锁之前,线程执行monitorenter指令进入Monitor对象的EntryList队列,此时会通过自旋尝试获得锁,如果自旋次数超过了一定阈值(默认10),才会升级为重量级锁,等待线程被唤起。
线程等待唤起的过程涉及到Linux系统用户态和内核态的切换,这个过程是很消耗资源的,自选锁的引入正是为了解决这个问题,先不让线程立马进入阻塞状态,而是先给个机会自旋等待一下。
(5)重量级锁
在2中已经介绍,就是通常说的synchronized重量级锁。
3.2 锁升级过程
锁升级的顺序为:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,且锁升级的顺序是不可逆的。
线程第一次获取锁获时锁的状态为偏向锁,如果下次还是这个线程获取锁,则锁的状态不变,否则会升级为CAS轻量级锁;如果还有线程竞争获取锁,如果线程获取到了轻量级锁没啥事了,如果没获取到会自旋,自旋期间获取到了锁没啥事,超过了10次还没获取到锁,锁就升级为重量级的锁,此时如果其他线程没获取到重量级锁,就会被阻塞等待唤起,此时效率就低了。
具体顺序如图所示,图摘自链接4: