Java 并发编程(四)JVM中锁的优化

前提知识—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,而是存放hashCodeGC分代年龄,同时锁对象标识位变为“0 01”(表示未锁定)。
这时Thread#2会来获取lockObject的轻量级锁,因为此时Thread#1和Thread#2交替进入临界区,所以偏向锁无法满足需求,需要膨胀到轻量级锁。

虚拟机在当前线程Thread#2的虚拟机栈中创建 Lock Record(栈中锁记录),Lock Record用来保存Mark Word的拷贝,然后使用CAS 操作将lockObjectMark Word 更新为 指向Lock Record 的指针。如果 CAS操作成功了,那么线程就获取了该对象上的锁,并且将lockObjectMark 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 并发编程(五)原子操作类

猜你喜欢

转载自blog.csdn.net/tubro2017/article/details/87880428