这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战
一、对象头
首先来看下 Java 对象结构。
对象中的几个部分的作用:
- 1、对象头中的 Mark word 主要用来表示对象的线程锁状态,另外还可以用来配合 GC、存放该对象的 hashCode
- 2、Klass Word 是一个指向方法区中 Class 信息的指针,意味着该对象可随时知道自己是哪个 class 的实例。
- 3、对象体是对象的成员属性
Synchronized 用的锁是存在Java对象里的,那么什么是Java 对象头呢?HotSpot虚拟机的对象头主要包括两部分数据:Mark Work(标记字段)和Klass Pointer(类型指针),虚拟机通过这个类型指针判断这个对象是哪个类的实例,Mark word默认存储对象的HashCode等运行时数据。
Java 对象处于 5 种不同状态时,Mark Word 中 64 个位的表现形式,上面每一行对象处于某种状态的 lock 标记如下
biased_lock lock 状态
0 01 无锁
1 01 偏向锁
00 轻量级锁
10 重量级锁
11 GC标记
二、轻量级锁
轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是 synchronized ,假设有两个方法同步块,利用同一个对象加锁。
下面通过这个例子来说明下轻量级锁的加锁原理。
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
1、每次指向到 synchronized 代码块时,先会在栈帧中创建 锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的 Mark Word 和对象引用 reference
2、让锁记录中的 Object reference 指向对象,并且尝试用 cas(compare and sweep) 替换 Object 对象的 Mark Word ,将 Mark Word 的值存入锁记录中。一开始 的01 是无锁状态,然后把 lock Record 的 lock 状态 00 (轻量锁)与 Object 对象的 01(正常)进行 CAS 替换(CAS 是原子性操作)。
3、如果 cas 替换成功,那么对象的对象头储存的就是锁记录的地址和状态 00 表示轻量级锁,这个对象就知道是哪个线程锁住我了,如下所示
4、如果cas失败,有两种情况
-
如果是其它线程已经持有了该 Object 的轻量级锁,那么表示有竞争,首先会进行自旋锁,自旋一定次数后,如果还是失败就进入锁膨胀阶段。
-
如果是自己的线程已经执行了 synchronized 进行加锁,那么再添加一条 Lock Record 作为重入的计数。只不过它在存储数据的时候会存放一个 null,代表加同个对象加锁的个数
5、解锁1: 当线程退出 synchronized 代码块的时候,如果获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
6、解锁2: 当线程退出 synchronized 代码块的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象头
- 成功则解锁成功,对象头状态变为00
- 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
三、 锁膨胀
前面说了,在加锁的时候发现其他线程对该对象加上了轻量级锁,这时CAS 操作失败,需要进行锁膨胀,将轻量级变为重量级锁。
1、当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁,此时加锁失败。
2、这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,
- 既为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址。
- 然后Thread-1自己进入 Monitor 的 EntryList Blocked 中阻塞。
3、当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给 对象头,因为 Object 指向的是 Monitor ,就会失败,所以会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为null,唤醒 EntryList 中 BLOCKED 线程,Thread-1 此时就参与重量级锁的竞争。
四、 自旋优化
重量级锁竞争时,还可以使用自旋来进行优化,就是让阻塞的线程循环查看几次,等待一段次数后再进入阻塞,如果当前线程自旋成功(即这时候锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 在Java 6 之后 自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就会自旋几次;反之,就会少自旋甚至不自旋。
- java 7 之后不能控制是否开启自旋功能。
五、 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程ID 是自己的就表示没有竞争,不用重新 CAS,以后只要不发生竞争,这个对象就归该线程所有。
5.1 偏向锁获取
“偏向”的意思是假定将来只有第一个申请锁的线程会使用锁(不会有任何线程来申请锁)。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后再进入退出的时候,直接把当前线程ID和Mark word的存储的线程id进行比较。一致就进入。如果Mark word的偏向锁的标识设置为1(当前是偏向锁),还是不死心,则尝试使用CAS去将Mark Word记录的线程id替换为自己的当前id。如果为0的话,表示当前无锁,用CAS去竞争锁。
5.2 偏向锁撤销
偏向锁使用了一种等待竞争出现才会释放锁的机制,所以当其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放锁。
1、偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)
2、暂停持有偏向锁的线程,判断锁对象是否还处于被锁定状态。
3、要么恢复到无锁(01)要么恢复到轻量级锁的状态(00)