前提知识—Java对象头(Mark Word)
锁的优化
偏向锁、轻量级锁、重量级锁三者各自的应用场景
- 偏向锁:只有一个线程进入临界区
- 轻量级锁:多个线程交替进入临界区
- 重量级锁:多个线程同时进入临界区
偏向锁、轻量级锁都是JVM引入的锁优化手段,目的是降低线程同步的开销。比如以下的同步代码块:
synchronized (lockObject) {
// do something
}
上述同步代码块中存在一个临界区,假设当前存在Thread#1和Thread#2这两个用户线程,分三种情况来讨论:
情况一:只有Thread#1会进入临界区;
情况二:Thread#1和Thread#2交替进入临界区;
情况三:Thread#1和Thread#2同时进入临界区。
情况一
上述的情况一是偏向锁的适用场景,此时当Thread#1进入临界区时,JVM会将lockObject
的对象头Mark Word
的锁标志位设为“1 01”,同时会用CAS
操作把Thread#1的线程ID记录到Mark Word
中,此时进入偏向模式。所谓“偏向”,指的是这个锁会偏向于Thread#1,若接下来没有其他线程进入临界区,则Thread#1再出入临界区无需再执行任何同步操作。也就是说,若只有Thread#1会进入临界区,实际上只有Thread#1初次进入临界区时需要执行CAS
操作,以后再出入临界区都不会有同步操作带来的开销(只需要比较对象头Mark Word
中线程ID是否为Thread#1)。
情况二
然而情况一是一个比较理想的情况,更多时候Thread#2也会尝试进入临界区。若Thread#2尝试进入时:
在Thread#1已退出临界区的情况(Thread#1不会主动去释放偏向锁),这时Thread#2发现Mark Word
的线程ID并不是自己,说明偏向锁上发生了竞争(对应情况二),此时会撤销偏向,Mark Word
中不再存放偏向线程ID,而是存放hashCode
和GC分代年龄
,同时锁对象标识位变为“0 01”(表示未锁定)。
这时Thread#2会来获取lockObject
的轻量级锁,因为此时Thread#1和Thread#2交替进入临界区,所以偏向锁无法满足需求,需要膨胀到轻量级锁。
虚拟机在当前线程Thread#2的虚拟机栈中创建 Lock Record
(栈中锁记录),Lock Record
用来保存Mark Word
的拷贝,然后使用CAS
操作将lockObject
的 Mark Word
更新为 指向Lock Record
的指针。如果 CAS
操作成功了,那么线程就获取了该对象上的锁,并且将lockObject
的 Mark Word
的锁标记变为 00,表示该对象处于轻量级锁状态(轻量级锁只能向上升级为重量级锁,或者直接解锁)。
Thread#2在退出临界区时(即同步完成)会通过CAS
操作尝试将Mark Word
的内容恢复(即解锁回到未锁定,但不可偏向的的锁状态“0 01”),会出现下面两种情况:
Mark Word
中的Lock Record
指针并不指向Thread#2虚拟机栈的Lock Record
,说明发生了竞争,CAS失败,轻量级锁膨胀为重量级锁(即情况3)Mark Word
中的Lock Record
指针指向Thread#2虚拟机栈的Lock Record
,顺利解锁,Thread#1进入临界区的时候就可以顺利获取轻量级锁。
轻量级锁CAS操作之前堆栈与对象的状态
轻量级锁CAS操作之后堆栈与对象的状态
情况三
若一直是Thread#1和Thread#2交替进入临界区,那么没有问题,轻量锁hold得住。一旦在轻量级锁上发生竞争,即出现“Thread#1和Thread#2同时进入临界区”的情况,轻量级锁就hold不住了。 需要使用互斥量来确保同步。
重量级锁、轻量级锁和偏向锁之间转换
该图主要是对上述内容的总结,如果对上述内容有较好的了解的话,该图应该很容易看懂。
Java 并发编程(一)Volatile原理剖析及使用
Java 并发编程(二)Synchronized原理剖析及使用
Java 并发编程(三)Synchronized底层优化(偏向锁与轻量级锁)
Java 并发编程(四)JVM中锁的优化
Java 并发编程(五)原子操作类