Monitor
解释synchronized同步远离之前,首先了解一下Monitor(翻译为监视器,又叫管程,基于操作系统)。
首先明确的是每一个Java对象都可以关联一个Monitor对象,当使用synchronized关键字的时候,该对象就会与一个Monitor对象关联,即该对象的Mark word中就会设置一个指向Monitor对象的指针(一个普通的对象包含64 bits的Object head、32 bits的Mark word 以及32 bits的Klass word)。
对象头格式:
重量级锁加锁原理
- 当一个线程 T1 尝试获取到锁了时,被synchronized指定的锁对象与一个Monitor产生了关联,此时如果锁对象没有被其他线程持有,那当前尝试获取锁的线程 T1 将成为这个Monitor的所有者,即Monitor对象中的owner描述的是线程 T1。
- 此时如果有其他线程 T2 尝试获取锁的时候,就会找synchronized指定的锁对象,看看这个锁对象关联的Monitor是谁,找到了Monitor就会查看该Monitor是否有owner的描述,也就是说看看是否有线程持有这个Monitor。此时发现该Monitor的持有者是线程 T1,此时线程 T2 将被记录在该Monitor的EntryList中,这个EntryList就是存储了在等待该锁资源的线程集合的描述,EntryList中的线程都将进入等待状态(阻塞)。
- 当持有Monitor的线程 T1 执行完临界区(加锁代码)业务之后,会释放了锁资源,即Minitor中owner的描述不再指向 T1,此时Monitor会将EntryList中的阻塞线程都唤醒(wake up),然后这些等待的线程将会竞争锁资源,会有一个新的线程称为Monitor的持有者。
轻量级锁
- 00 代表轻量级锁
- 10 代表重量级锁
- 01 代表没有枷锁
使用场景:如果一个对象被多线程访问,但是访问它的多线程的时间是错开的,并不存在同一时间竞争的情况,就可以使用轻量级锁来优化处理;轻量级锁的使用仍然是通过synchronized关键字。
- 当使用synchronized关键字之后,首先在线程的栈帧中创建一个Lock Record,Lock Record中包含两部分——(Object reference),(lock record 地址 00),用于记录锁对象的引用以及锁对象的Mark word。加锁的时候,首先Object reference记录了锁对象的引用,然后用(lock record 地址 00)与锁对象的Mark word(hashcode age bias 01)交换;如果交换成功,就是锁已经加上了,此时锁对象的Mark word中是(lock record 地址 00),要注意的是只有锁对象Mark word中是 01 的时候才能加锁成功。这个交换的过程就是CAS(原子操作)。
- 如果加锁的过程中,锁对象的Mark word不是 01,已经有其他线程持有了该锁对象,此时加锁失败,进入锁膨胀状态
- 如果是当前线程发生了锁重入的情况,就会再添加一条Lock record作为记录,新记录中(Object reference)同样需要记录锁对象的引用,但是交换锁对象的Mark word失败(实际上是在交换的过程中,能判断出锁对象Mark Word已经和本线程的一条Lock record交换过了,就不再进行交换了,直接为null)。
- 当释放锁的时候,如果有Lock record记录中为null的记录,表示有锁重入,清掉这条Lock record就行了,重入计数【减1】,直到释放到Lock record不为null的时候,就要开始做真正的锁释放:还原锁对象的Mark word,再交换回去,就解锁成功了。
- 如果解锁失败,说明轻量级进行了锁膨胀或者已经升级为重量级锁,此时就要进入重量级锁解锁流程
锁膨胀
如果在加轻量级锁的过程中,CAS失败了,也就是说已经有其他线程为该锁对象已经加上了轻量级锁,此时出现了线程间的锁竞争,这是需要将轻量级锁升级为重量级锁,这就是锁膨胀。当出现锁膨胀的时候,就回到了synchronized加锁原理的情况。
如下:
- t1 目前对 object 加上了轻量级锁,交换了 mark word,此时 t2 对 object 进行CAS操作失败,进入锁膨胀流程。
- t2 为 object 申请 Monitor,即 object 的Mark word指向了 Monitor地址,此时变成了(monitor 10)。
- t2 进入 Monitor 的EntryList,开始阻塞。
重量级锁解锁流程
很明显 t1 在释放锁的时候,无法还原 object 的mark word,因为交换不回去了,object 的mark word 已经变成(monitor 10)了。
- t1 根据 object 的mark word 找到 monitor,设置 monitor的owner为空,
- 唤醒EntryList里面等地的线程,开始新一轮的竞争
自旋优化
上面在描述重量级锁的时候,提到了竞争失败锁对象的线程将进入Monitor的EntryList阻塞,其实这是简单的说法;实际上在进入EntryList之前,这些线程还进行了自旋优化。
什么是自旋优化,指的就是在竞争失败锁资源之后,在进入EntryList之前,线程会进行若干次获取锁资源的重试操作。
如果当前持有锁对象的线程执行的很快,可能就在自旋重试的过程中,就释放了锁,被处于自旋重试过程的线程获取到了,就不用进入EntryList,不用进入阻塞状态,也就避免了一些线程间切换的情况。当然自旋优化只能在多核cpu上使用。
- java6之后自旋重试是自适应的,也就是说该锁对象上的线程自旋重试成功率大,就会多自旋几次,成功率小就会减少自旋次数,甚至不自旋。
- 单核自旋优化没有意义
- java7之后不能设置是否开启自旋优化
偏向锁
在重入锁的情况下,轻量级锁每次重新获取锁对象的时候还是需要CAS操作。
为了优化这些开销,java6之后引入了偏向锁来进一步优化加锁的开销:只有第一次获取锁的时候,通过CAS将线程的id设置到锁对象Mark word,之后的重入动作,发现线程id就是本线程之后,就不会执行CAS。只要不发生锁竞争,这个锁对象就归这条线程持有。
一个对象创建时:
- 如果开启偏向锁(默认开启),那么对象创建后Mark word的最后三位是 1 01,这时thread、epoch、age都是0。
- 如果没有开启偏向锁,那么对象创建后Mark word的最后三位是 0 01,hread、epoch、age都是0
/*
* 功能描述: <br>
* 〈验证偏向锁对象头的mark word信息,需要借助jol-core包〉
* @Param: []
* @Return: void
* @Author: LeoLee
* @Date: 2020/12/1 15:47
*/
public void testBiasedMarkword() {
log.info(ClassLayout.parseInstance(new Person()).toPrintable());
}
class Person {
}
public static void main(String[] args) {
BiasedTest biasedTest = new BiasedTest();
biasedTest.testBiasedMarkword();
}
执行结果:
15:52:24.893 [main] INFO com.leolee.multithreadProgramming.concurrent.syn.BiasedTest - com.leolee.multithreadProgramming.concurrent.syn.BiasedTest$Person object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 94 ff 00 f8 (10010100 11111111 00000000 11111000) (-134152300)
12 4 com.leolee.multithreadProgramming.concurrent.syn.BiasedTest Person.this$0 (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
为什么最后三位不是1 01呢?不是说好默认是开启偏向锁的嘛
- 偏向锁是有延迟的,不会在程序启动的时候立刻生效,如果想避免这种情况,可以添加VM参数: -XX:BiasedLockingStartupDelay=0 来禁用延迟,也可以在程序启动之后sleep一下,等待偏向锁生效
修改代码增加延迟:
/*
* 功能描述: <br>
* 〈验证偏向锁对象头的mark word信息,需要借助jol-core包〉
* @Param: []
* @Return: void
* @Author: LeoLee
* @Date: 2020/12/1 15:47
*/
public void testBiasedMarkword() throws InterruptedException {
log.info(ClassLayout.parseInstance(new Person()).toPrintable());
Thread.sleep(4*1000);
log.info(ClassLayout.parseInstance(new Person()).toPrintable());
}
执行结果如下:
配置VM参数:
测试偏向锁特性
修改代码如下:
/*
* 功能描述: <br>
* 〈验证偏向锁对象头的mark word信息,需要借助jol-core包〉
* @Param: []
* @Return: void
* @Author: LeoLee
* @Date: 2020/12/1 15:47
*/
public void testBiasedMarkword() throws InterruptedException {
Person person = new Person();
//Thread.sleep(5*1000);//偏向锁有延迟性,并不会在程序启动后马上生效,所以sleep,也可以添加VM参数: -XX:BiasedLockingStartupDelay=0 来禁用延迟
log.info(ClassLayout.parseInstance(person).toPrintable());
synchronized (person) {
log.info(ClassLayout.parseInstance(person).toPrintable());
}
log.info(ClassLayout.parseInstance(person).toPrintable());
}
执行结果:
16:17:20.587 [main] INFO com.leolee.multithreadProgramming.concurrent.syn.BiasedTest - com.leolee.multithreadProgramming.concurrent.syn.BiasedTest$Person object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 94 ff 00 f8 (10010100 11111111 00000000 11111000) (-134152300)
12 4 com.leolee.multithreadProgramming.concurrent.syn.BiasedTest Person.this$0 (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
16:17:20.590 [main] INFO com.leolee.multithreadProgramming.concurrent.syn.BiasedTest - com.leolee.multithreadProgramming.concurrent.syn.BiasedTest$Person object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 fd 02 (00000101 00111000 11111101 00000010) (50149381)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 94 ff 00 f8 (10010100 11111111 00000000 11111000) (-134152300)
12 4 com.leolee.multithreadProgramming.concurrent.syn.BiasedTest Person.this$0 (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
16:17:20.591 [main] INFO com.leolee.multithreadProgramming.concurrent.syn.BiasedTest - com.leolee.multithreadProgramming.concurrent.syn.BiasedTest$Person object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 fd 02 (00000101 00111000 11111101 00000010) (50149381)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 94 ff 00 f8 (10010100 11111111 00000000 11111000) (-134152300)
12 4 com.leolee.multithreadProgramming.concurrent.syn.BiasedTest Person.this$0 (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以看到Person对象一直都是 101,是一个偏向锁,当加锁执行后,Person对象的Mark word中后面几位变化了,那就是指向的线程id(和Java中Thread.getId()不一样,这是操作系统中线程的标识),并在锁释放后,这个线程id还是存在与Mark word中,这就证明了偏向锁的偏向性:只要没有其他线程的资源竞争,这个锁对象就归这条线程持有。
禁用偏向锁
设置VM参数:-XX:-UseBiasedLocking 可禁用偏向锁
当偏向锁禁用之后,该对象被一条线程持有之后,将变成轻量级锁:
使用HashCode导致偏向锁失效
/*
* 功能描述: <br>
* 〈验证偏向锁对象头的mark word信息,需要借助jol-core包〉
* @Param: []
* @Return: void
* @Author: LeoLee
* @Date: 2020/12/1 15:47
*/
public void testBiasedMarkword() throws InterruptedException {
Person person = new Person();
//偏向锁有延迟性,并不会在程序启动后马上生效,所以sleep,也可以添加VM参数: -XX:BiasedLockingStartupDelay=0 来禁用延迟
//可以使用-XX:-UseBiasedLocking 禁用偏向锁,此时该对象加锁后会编程 000
//Thread.sleep(5*1000);
//获取hashcode后导致偏向锁失效
log.info(String.valueOf(person.hashCode()));
log.info(ClassLayout.parseInstance(person).toPrintable());
synchronized (person) {
log.info(ClassLayout.parseInstance(person).toPrintable());
}
log.info(ClassLayout.parseInstance(person).toPrintable());
}
执行结果:
此时后面几位代表Hashcode
偏向锁的撤销
- 多个线程访问同一个对象,偏向锁被撤销,升级为轻量级锁
/*
* 功能描述: <br>
* 〈测试多个线程访问同一个对象,偏向锁被撤销〉
* @Param: []
* @Return: void
* @Author: LeoLee
* @Date: 2020/12/1 16:57
*/
public void testBiasedLockUnusable() {
Person person = new Person();
Thread t1 = new Thread(() -> {
log.info(ClassLayout.parseInstance(person).toPrintable());
synchronized (person) {
log.info(ClassLayout.parseInstance(person).toPrintable());
}
log.info(ClassLayout.parseInstance(person).toPrintable());
}, "t1");
Thread t2 = new Thread(() -> {
try {
//保证t2在t1执行完成之后再执行
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(ClassLayout.parseInstance(person).toPrintable());
synchronized (person) {
log.info(ClassLayout.parseInstance(person).toPrintable());
}
log.info(ClassLayout.parseInstance(person).toPrintable());
}, "t2");
t1.start();
t2.start();
}
执行结果:
- 使用 wait() 和 notify() 也会撤销偏向锁,因为这两个是重量级锁才有的方法
批量重偏向
如果锁对象被多个线程没有竞争的访问,原先偏向线程t1的对象还是可以偏向t2的,这就是重偏向,此时会重置Mark word中的线程id。
注意:当一个对象的偏向锁被撤销次数过多,超过了一定的阈值,JVM会考虑这是否是偏向错误了,是不是应该偏向另外的线程,所以这时候就会触发重偏向。阈值为20。
因为撤销偏向也是一种性能的开销,所以才有此机制来重新设定偏向的线程。
代码省略了,自行模拟20次以上没有竞争的其他线程获取偏向锁对象。
批量撤销偏向
当撤销偏向的次数超过阈值40的时候,对于此类所产生的对象竞争过于激烈,JVM将考虑不再对此类实例化出来的对象添加偏向锁,包括新建的对象。
锁消除
java程序在运行的时候,有一个JIT(即时编译器),实际就是对开发者的代码进行一定程度的优化,这个优化过程中,可能在一定情况下,优化掉synchronized。
比如一个同步代码块,锁对象是在方法内的局部变量,并且此局部变量不会发生变量逃离,此时JIT就会把synchronized优化掉。此时加锁的代码和不加锁的代码执行没有区别(性能也是没有区别的)。