目录
- java对象头
- 一:Monitor
- 二:sychronized的优化
- 轻量级锁
- (轻量级锁)锁膨胀(重量级锁)
- (重量级锁)锁自旋
- 偏向锁(比轻量级锁更轻量)
- 偏向锁状态
- 如何撤销偏向锁
- 批量重偏向
- 批量撤销
- 锁擦除
java对象头
一:Monitor
- Monitor翻译过来可以叫监视器和管程。
- 当给某个对象加上锁之后,这个对象的对象头中的MarkWord就会指向一个Monitor。
结构如下
-
刚开始OWner为空,当有线程获得锁时,owner就会指向该线程;
-
当有其他线程执行到sychronized时,因为一个owner只能指向一个线程,所以它就会进入EntryList进入BLOCKED状态
-
当线程执行完释放锁时,会通知Monitor,Monitor会唤醒EntryList中的线程,然后这些线程会进行非公平竞争来获取锁;
注意:
-
一个对象只有一个Monitor
二:sychronized的优化
-
故事
-
故事角色
- 老王 - JVM
- 小南 - 线程
- 小女 - 线程
- 房间 - 对象
- 房间门上 - 防盗锁 - Monitor
- 房间门上 - 小南书包 - 轻量级锁
- 房间门上 - 刻上小南大名 - 偏向锁
- 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
- 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包
轻量级锁
- 当一个对象有多线程要加锁,但是多线程加锁的时间是错开的,这个时候我们可以使用轻量级锁来优化
- 轻量级锁是透明的,即我们还是使用sychronized方法进行加锁;
我们研究一下一段代码:
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
- 这里加锁的流程:
- 刚开始加锁会在线程的栈帧中加入一个锁记录对象,然后将锁记录中的对象引用指向加锁对象的地址;
- 然后尝试用cas替换对象头中的MarkWord,将MarkWord存入锁记录;
- 如果cas成功,对象头中就保存了锁地址和状态00;表示该线程给对象加锁
- cas失败有两种情况:
- 1:如果其他线程已经持有了Object轻量级锁,说明具有锁竞争,进入锁膨胀状态;
- 2:如果是synchronizad锁重入,那么就会在加一条lock record记录重入的次数,这里的锁记录为null;
- 然后继续指向,再次对这个对象加锁,就会形成synchronized锁重入现象,创建一个lock recode记录重入次数:
- 当解锁时,如果lock record的锁记录为空那么就删除该锁记录,同时重入次数-1;
- 如果解锁是,lockrecord的锁记录不为空,那么就会使用cas将MarkWord恢复给对象
- 成功
- 失败:说明进行了所膨胀或者是升级为重量级锁,进入重量级锁解锁流程;
(轻量级锁)锁膨胀(重量级锁)
- 在cas失败,也就是已经有线程持有object锁对象了,那么就会进入锁膨胀,将轻量级锁转变为重量级锁。
- 首先Object会申请Monitor,然后object会指向Monitor的地址,然后将线程加入到EntryList进入BLOCKED状态进行等待;
- 然后原本持有轻量级锁的线程使用cas将MarkWord还给对象时会失败,然后就会根据Monitor的地址找到Monitor,将Owner置为空,然后唤醒EntryListBLOCKED的线程;
(重量级锁)锁自旋
在重量级锁竞争过程中,会通过自旋(循环获取重量级锁)来进行优化,如果获取锁成功(持锁线程释放了锁),就可以避免进入阻塞状态(从阻塞再恢复会进行上下文切换,比较耗费性能)
注意:
- 自旋回占用cpu资源,单核进行自旋没有意义,多核才有效果
- java6 之后的锁自旋是很智能的;
- java7之后可以开启或者关闭锁自旋;
偏向锁(比轻量级锁更轻量)
- 我们在使用轻量级锁在没有竞争时进行锁重入的时候,还是会进行cas操作;
- 我们可以使用偏向锁,偏向锁只会在第一次获取锁时使用cas将线程的ID设置为对象头中的MarkWord,之后在没有竞争的情况下,发现线程id是线程本身的话就不会进行cas操作,对象属于该线程;
偏向锁状态
-
一个对象在创建时:
- 默认是可以使用偏向锁,他的MarkWord后三位默认是101,其他位都是0;
- 但是偏向锁开启默认都是延时的,想要避免延时可以加上Vm参数:-XX:BiasedLockingStartupDelay=0
来
禁用延迟 - 如果禁用了偏向锁,那么创建对象之后的MarkWord后三位是001;
我们可以来测试偏向锁:
public static void main(String[] args) throws IOException {
Dog d = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(d);
new Thread(() -> {
log.debug("synchronized 前");
System.out.println(classLayout.toPrintableSimple(true));
synchronized (d) {
log.debug("synchronized 中");
System.out.println(classLayout.toPrintableSimple(true));
}
log.debug("synchronized 后");
System.out.println(classLayout.toPrintableSimple(true));
}, "t1").start();
}
输出:
11:08:58.117 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
这里关闭了延时所以刚开始就是101,然后加上偏向锁之后在Markword中就多了线程ID,之后就是释放了锁也是有线程ID,这个就是偏向,加锁之后就属于该线程了;
如果禁用偏向锁:
11:13:10.018 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
加锁之后默认使用的是轻量级锁,是00;
在调用hashcode之后会车撤销偏向锁,因为对象头中没有位置;
我们知道锁的优先级是偏向锁,轻量级锁,重量级锁;
如何撤销偏向锁
- 使用hashcode
- 其他线程来获取锁(不是同时来竞争。出现竞争就会转变成重量级锁了)
- 使用wait/notify
批量重偏向
- 当对象被多线程访问,但是没有竞争时,此时偏向于t1线程的偏向锁有机会偏向t2线程,会重置线程ID;
- 当撤销偏向锁的阈值超过20次时,jvm会认为偏向锁偏向错了,会在加锁时重新偏向于新线程;
批量撤销
- 当撤销锁的阈值超过40次时,Jvm就会判断,是不是不应该偏向,于是这个类的所有对象都变成了不可偏向,新建的对象也都是不可偏向;
锁擦除
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
static int x = 0;
@Benchmark
public void a() throws Exception {
x++;
}
@Benchmark
public void b() throws Exception {
//这里的o是局部变量,不会被共享,JIT做热点代码优化时会做锁消除
Object o = new Object();
synchronized (o) {
x++;
}
}
}
- 这里要判断加锁和不加锁的性能差距,最后得出的结果是差不多的,为什么呢
- 因为JIT即时编译器会对热点代码进行优化,这里他就会判断o变量是局部变量,没有逃出方法的作用范围不会被共享。是线程安全的所以会对其进行优化擦去锁;