1. 启蒙知识预热:CAS 原理 + JVM 对象头内存存储结构
2.JVM 中锁优化:锁粗化、锁消除、偏向锁、轻量级锁、自旋锁。
3. 总结:偏向锁、轻量级锁,重量级锁的优缺点。
开启本文之前先介绍 2 个概念
为了提高性能,JVM 很多操作都依赖 CAS 实现,一种乐观锁的实现。本文锁优化中用到了 CAS,故有必要先分析一下 CAS 的实现。
CAS:Compare and Swap。
JNI 来完成 CPU 指令的操作:
unsafe.compareAndSwapInt(this, valueOffset, expect, update);
CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。
打开源码:openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp,如下图:0
os::is_MP() 这个是 runtime/os.hpp,实际就是返回是否多处理器,源码如下:
如上面源代码所示(看第一个 int 参数即可),LOCK_IF_MP: 会根据当前处理器的类型来决定是否为 cmpxchg 指令添加 lock 前缀。如果程序是在多处理器上运行,就为 cmpxchg 指令加上 lock 前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略 lock 前缀(单处理器自身会维护单处理器内的顺序一致性,不需要 lock 前缀提供的内存屏障效果)。
HotSpot 虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 本文只讲对象头。
HotSpot 虚拟机的对象头 (Object Header) 包括两部分信息:
第一部分 "Mark Word": 用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等.
第二部分 "类型指针":对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。)
32 位的 HotSpot 虚拟机对象头存储结构:(下图摘自网络)
图 1 32 位的 HotSpot 虚拟机对象头
为了证实上图的正确性,这里我们看 openJDK--》hotspot 源码 markOop.hpp,虚拟机对象头存储结构:
图 2 HotSpot 源码 markOop.hpp 中注释
源码中对锁标志位这样枚举:
- 1 enum {
- locked_value = 0,
- //00 轻量级锁
- 2 unlocked_value = 1,
- //01 无锁
- 3 monitor_value = 2,
- //10 监视器锁,也叫膨胀锁,也叫重量级锁
- 4 marked_value = 3,
- //11 GC标记
- 5 biased_lock_pattern = 5 //101 偏向锁
- 6
- };
下面是源码注释:
图 3 HotSpot 源码 markOop.hpp 中锁标志位注释
看图 3,不管是 32/64 位 JVM,都是 1bit 偏向锁 + 2bit 锁标志位。上面红框是偏向锁(一个是指向给定线程的显示偏向锁,一个是匿名偏向锁)对应枚举 biased_lock_pattern,下面红框是 4 种请求头结构。分别对应上面的前 4 种枚举。我们甚至能看见锁标志 11 时,是 GC 的 markSweep(标记清除算法) 使用的。(这里就不再拓展了)
==================================================================
大家都知道 java 中锁 synchronized 性能较差,线程会阻塞。
在 jdk1.6 中对锁的实现引入了大量的优化来减少锁操作的开销:
- 锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。锁消除(Lock Elimination):通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。偏向锁(Biased Locking):无锁竞争的情况下跳过轻量级锁,即不执行CAS原子指令。适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁),进入到阻塞状态。
- 按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象"偏向"这个线程,之后的多次调用则可以避免CAS操作。
- 简单的讲,就是在锁对象的对象头(开篇讲的对象头数据存储结构)中有个ThreaddId字段,这个字段如果是空的,第一次获取锁的时候,就将自身的ThreadId写入到锁的ThreadId字段内,将锁头内的是否偏向锁的状态位置1.这样下次获取锁的时候,直接检查ThreadId是否和自身线程Id一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段。提高了效率。
- 注意:当锁有竞争关系的时候,需要解除偏向锁,进入轻量级锁。
每一个线程在准备获取共享资源时:
- 获得偏向锁如下图:
如上图所示:
注意点:JVM 加锁流程
偏向锁 --》轻量级锁 --》重量级锁
从左往右可以升级,从右往左不能降级
本文重点介绍了 JVM 对 Synchronized 的优化,但竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过 - XX:-UseBiasedLocking 来禁用偏向锁。下面是这几种锁的对比:
锁 |
优点 |
缺点 |
适用场景 |
偏向锁 |
加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 |
如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 |
适用于只有一个线程访问同步块场景。 |
轻量级锁 |
竞争的线程不会阻塞,提高了程序的响应速度。 |
如果始终得不到锁竞争的线程使用自旋会消耗 CPU。 |
追求响应时间。 同步块执行速度非常快。 |
重量级锁 |
线程竞争不使用自旋,不会消耗 CPU。 |
线程阻塞,响应时间缓慢。 |
追求吞吐量。 同步块执行速度较长。 |
==========================
参考:
《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》第二版 JDK1.7(本文 JDK1.8,内容无冲突)
来源: http://www.cnblogs.com/dennyzhangdd/p/6734638.html