synchronized 锁升级实现原理
对象的内存结构
在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充
我们需要重点分析MarkWord对象头
MarkWord
- hashcode:25位的对象标识Hash码
- age:对象分代年龄占4位
- biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁
- thread:持有偏向锁的线程ID,占23位
- epoch:偏向时间戳,占2位
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位
我们可以通过lock的标识,来判断是哪一种锁的等级
- 后三位是001表示无锁
- 后三位是101表示偏向锁
- 后两位是00表示轻量级锁
- 后两位是10表示重量级锁
2.2.3 再说Monitor重量级锁
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
简单说就是:每个对象的对象头都可以设置monoitor的指针,让对象与monitor产生关联
2.2.4 轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
static final Object obj = new Object();public static void method1() {synchronized (obj) {// 同步块 Amethod2();}
}public static void method2() {synchronized (obj) {// 同步块 B}
}
加锁的流程
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现
这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
static final Object obj = new Object();public static void m1() {synchronized (obj) {// 同步块 Am2();}
}public static void m2() {synchronized (obj) {// 同步块 Bm3();}
}public static void m3() {synchronized (obj) {}
}
加锁的流程
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些
实现全流程
在java6.0之前我们使用synchronized是为了解决多个线程之间的竞争问题,使用的是重量级锁。其实现是通过监视器Monitor来实现。但在java6之后就引入了偏向锁,轻量级锁,以及重量级锁。下面是锁升级的全流程介绍
首先当一个对象刚被创建在其对象头当中会设置Mark Word信息,Mark Word是用来保存对象的基本信息如hash值,锁信息等。当一个对象刚被创建其状态必然是无锁状态,在对象头的Mark Word当中就会有个所表示用来表示当前状态,无锁为00.当一个线程尝试获取锁时,就会检查Mark Word当中的信息判断其状态,如果为无锁状态就会尝试通过CAS的方式去修改锁表示,在CAS执行成功后同时还会在Mark Word当中储存当前线程的线程ID,之后会在锁的内存当中创建一条所记录用于备份对象头当中的Mark Word信息。
此时如果还有其他线程进入,首先回去判断Mark Word当中的线程ID与自身线程ID是否相同,如果不同则表明出现了锁竞争,jvm会将当前锁升级为轻量级锁。在轻量级锁当中对象的Mark Word会保存一个锁对象的地址指针,线程需要用过CAS的方式去修改这个指针使其指向自己,哪个线程修改成功就可以获取锁。除此之外轻量级锁同偏向锁一样也会在线程内部创建所记录,但是轻量级锁的所记录条数取决于重入次数(只有第一次所记录才会记录对象的Mark Word信息,重入锁的所记录不会记录Mark Word)。
如果其他线程多次尝试获取锁失败,说明当前竞争非常激烈,此时jvm就会将锁升级为重量级锁,重量级锁是通过监视器Monitor实现的,锁对象的对象头会通过Mark Wrld指向一个监视器。在监视器的内部主要包含三个组成,Owner,EntitySet,WaitSet。当线程尝试获取锁的时候就会去检查Owner当中是否为空,如果为空则尝试将Owner与自身线程绑定,即占有了该锁。此时如果还会有线程进入一样需要查看Owner当中是否为空,如果不为空则会被放入EntitySet当中,该结构是用来储存等待的线程的。当Owner线程执行完毕释放锁,就会通过计算机操作系统,通过内核态来唤醒这些线程去抢锁。除此之外对于使用了Wait方法的线程,监视器会将其放入WaitSet队列当中进行等待
Monitor实现的锁属于重量级锁,你了解过锁升级吗?
- Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。