Java锁
在Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:
无锁状态
偏向锁状态
轻量级锁状态
重量级锁状态
几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级发生的条件会比较苛刻,锁降级发生在Stop The World期间,当JVM进入安全点的时候,会检查是否有闲置的锁,然后进行降级。
每个Java对象都有对象头。如果是非数组类型,则用2个字宽来存储对象头,如果是数组,则会用3个字宽来存储对象头。在32位处理器中,一个字宽是32位;在64位虚拟机中,一个字宽是64位。 对象头的内容如下表:
32/64bit
Mark Word
存储对象的hashCode或锁信息等
32/64bit
Class Metadata Address
存储到对象类型数据的指针
32/64bit
Array length
数组的长度(如果是数组)
我们主要来看看Mark Word的格式:
无锁
0
01
偏向锁
线程ID
1
01
轻量级锁
指向栈中锁记录的指针
此时这一位不用于标识偏向锁
00
重量级锁
指向互斥量(重量级锁)的指针
此时这一位不用于标识偏向锁
10
GC标记
此时这一位不用于标识偏向锁
11
可以看到:
当对象状态为偏向锁时,Mark Word存储的是偏向的线程ID;
当状态为轻量级锁时,Mark Word存储的是指向线程栈中Lock Record的指针;
当状态为重量级锁时,Mark Word为指向堆中的monitor对象的指针。
总结锁的升级流程: 每一个线程在准备获取共享资源时:
第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
第三步,两个线程都把锁对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord。
第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋 。
第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败 。
第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。
各种锁的优缺点对比 [下表来自《Java并发编程的艺术》]:
偏向锁
加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距
如果线程间存在锁竞争,会带来额外的锁撤销的消耗。
适用于只有一个线程访问同步块场景
轻量级锁
竞争的线程不会阻塞,提高了程序的响应速度。
如果始终得不到锁竞争的线程使用自旋会消耗CPU
追求响应时间. 同步块执行速度非常快
重量级锁
线程竞争不使用自旋,不会消耗CPU。
线程阻塞,响应时间缓慢。
追求吞吐量. 同步块执行时间较长
Last updated