JVM锁优化
1.锁优化
- 挂起线程和恢复线程的开销较大,对于锁定状态时间较短的情况下,挂起线程并不值得。
- 自旋锁与它的自适应自旋
- 遇到锁不会挂起,而是忙循环(自旋)一会儿,避免了一次线程切换的开销,但是仍在占用CPU时间。
- 1.6默认开启,默认自旋10次。
- 1.6还引入了自适应自旋锁,他可以根据上一次在同一个锁上的自旋时间调整自旋次数。
- 自旋失败则进入正常的挂起线程。
- 锁消除
- JIT即时编译器在运行时如果发现某块代码上有同步,但是检测到该共享区域不可能存在竞争,就会进行锁消除。
- 如对某个局部变量操作时加了锁,但是局部变量不可能逃逸出方法,所以2个线程不可能对同一个局部变量存在竞争。此时就会消除锁。
- JIT编译说明这段代码是热点代码,消除锁之后对性能提高有所帮助。
- 锁粗化
- 对一个对象反复的加锁解锁。如StringBuff的append()方法,编译器会优化到最外面一个锁。
- 轻量级锁
- jvm实现无竞争情况下使用CAS操作消除同步使用的互斥量。
- 轻量级锁为什么可以提高性能就是因为 “绝大多数部分的锁在整个同步周期内都是不存在竞争的”,这是一个经验数据,如果不存在竞争使用CAS操作就可以避免使用同步互斥量的开销。
- 偏向锁
- 轻量级锁是无竞争情况下使用CAS操作取消使用同步互斥量,而偏向锁是无竞争情况下取消整个同步,连CAS都不用做。
- 偏向锁会偏向第一个获得他的线程,如果该锁没有被其他线程获取,那么持有偏向锁的线程就不会进行同步。
2.对象头
- 我们所说的某个对象的锁其实就是该对象的对象头中的几个标志位,该标志位改变为某个值说明该对象的锁被线程拿走了,释放锁后标志位恢复。
- 对象头分为2部分,第一部分存储对象自身运行时数据,第二部分存储类型指针。
- 自身运行时数据(Mark Word):如哈希码、GC分代年龄、锁转态标志、线程持有的锁,偏向线程ID、偏向时间戳。Mark Word是非固定的数据结构,以便存储更多信息,根据对象状态不同各个信息所占位数会变化,但总体肯定是8字节倍数。32位机下Mark Word占32bit,64位机下占64bit。
- 类型指针:指向元数据(Class类数据)的指针
- 如果对象是数组那么对象头还有一块用于记录数组长度的区域,因为普通Java对象通过元数据可以确定大小,而数组的元数据无法确定数组大小。
2.1 Mark Word 的不同状态下存储不同内容。
状态 | 标志位 | 存储内容 |
未锁定 | 01 | 对象哈希码。对象分代年龄 |
轻量级锁定 | 00 | 指向锁记录的指针 |
膨胀(重量级锁定) | 10 | 指向重量级锁的指针 |
GC标记 | 11 | 空,不需要记录信息 |
可偏向 | 01 | 偏向线程ID、偏向时间戳、对象分代年龄 |
2.2 不同状态下不同信息所占的位数和位置
3.轻量级锁的实现和加锁过程。
- 程序并不是一遇到同步代码块立刻就拿到对象的重量级锁。
-
加锁
- 代码进入同步块时如果锁对象的标志位时01那么虚拟机会在当前线程的栈帧中建立一个 锁记录 空间 Lock Record,用来存储锁对象目前的Mark Word 的拷贝。
- 然后执行CAS 操作 ,它会比较锁对象的Mark Word 与拷贝是否相等。如果相等执行3,如果不等执行5
- 如果相等将锁对象的Mark Word 中的存储内容替换为 指向栈帧中拷贝的指针,此时这个线程就获得了该对象的锁,并且对象的锁标志位改为00。
- 此时栈帧中的Mark Word 存储的内容是(对象的hashCode,分代年龄,偏向锁位)而锁对象Mark Word 存储的内容是(指向栈中锁记录的指针),而且前者锁标志位01,后者锁标志位00.
- 如果不相等首先检查锁对象Mark Word内容是否指向当前线程,如果指向执行6,如果没有指向执行7
- 如果指向说明当前线程已经拿到了锁,则可以进入同步代码块执行,比如同步方法中调用同步方法,并且使用的同一个锁对象。
- 如果没有指向当前线程说明锁已经被别的线程拿到了,此时锁标志位改为10,锁对象Mark Word内容换为指向互斥量(重量级锁synchronized)的指针,然后执行8
- 注意此时线程发现拿不到锁也不会立刻被挂起,它会加入自旋,如果自旋一定次数失败就会进入阻塞状态。
-
解锁
- 首先查看锁对象的Mark Word内容是否指向栈,如果是那么就交换两者,同步代码执行完成。
- 如果没有,说明有别的线程来拿过锁,所以要解除互斥量同时唤醒被挂起的线程。
- synchronized是不公平的不会按先后挂起顺序唤醒。
4.偏向锁
- -XX:+UseBiasedLocking 启用偏向锁,1.6默认
- 锁对象第一次被线程获得,会把标志位设为01,此时线程ID还是空,对象还未锁定
- 通过一次CAS操作会把获取到这个锁对象的线程ID存入锁对象的Mark Word。此时对象已被锁定。
- 之后持有偏向锁的线程进入同步代码就不需要任何同步操作了。
- 如果另一个线程来获取锁,那么偏向模式就结束,分别如图按2种不同情况作出不同反应。