【多线程:synchronized优化原理】
01.介绍
synchronized优化主要是在四个方面:重量级锁,轻量级锁,自旋锁,偏向锁,接下来的内容都会讲解这些锁。
Mark Word结构
Mark Word(32bits) | 加锁状态 | State |
---|---|---|
hashcode:25 age:4 biased_lock:0 | 01 | Normal |
thread:23 epoch:2 age:4 biased_lock:1 | 01 | Biased |
ptr_to_lock_record:30 | 00 | Lightweight Locked |
ptr_to_heavyweight_montior:30 | 10 | Heavyweight Locked |
11 | Marked for GC |
小故事(黑马juc的例子)
故事角色
老王 - JVM/操作系统
小南 - 线程
小女 - 线程
房间 - 对象
房间门上 - 防盗锁 - Monitor
房间门上 - 小南书包 - 轻量级锁
房间门上 - 刻上小南大名 - 偏向锁
批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,
即使他离开了,别人也进不了门,他的工作就是安全的。但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女
晚上用。每次上锁太麻烦了,有没有更简单的办法呢?小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因
此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是
自己的,那么就在门外等,并通知对方下次用锁门的方式。后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍
然觉得麻烦。于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那
么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦
掉,升级为挂书包的方式。同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老
家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老
王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包
这个故事就是类比与上述几种锁,各位可以在理解不清楚时 回来类比看看。
把握住一个点:轻量级锁
轻量级锁如果开始竞争 则变为重量级锁
轻量级锁如果长时间是自己一个线程 则变为偏向锁
轻量级锁 在变为重量级锁的过程中 竞争的线程并不会直接进入Monitor的阻塞列表 而是会先变为自旋锁 看是否有机会直接获取锁
02.重量级锁
重量级锁就是我们上个文章讲的Monitor,详情可以看上个文章。
03.轻量级锁
应用场景
如果一个对象虽然有多个线程访问,但是多线程访问的时间是错开的(也就是没有竞争),那么可以用轻量级锁来优化,有同学可以会有疑惑既然都没有竞争了为什么要用多线程,因为只是大部分时间是没有竞争 还是会出现有竞争的情况 此时就要把锁变为重量级锁
注意
轻量级锁对使用者是透明的,也就是语法上还是synchronized
例子
假设有两个方法同步块,利用一个对象加锁
Thread-0线程
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
// 同步块A
method2();
}
}
public static void method2(){
synchronized(obj){
// 同步块B
}
}
解释
上述代码的目的是用来模拟轻量级锁,并用下面的图片解释
本图含义为,我们又一个线程Thread0 有一个监视器Object,可以看出线程的地址初始化是00 代表着轻量级锁也就是MarkWord里的加锁状态,然后 对象的对象头中MarkWord是Hsahcode Age Bias 01也就是normal状态
接下来 我们可以看到 通过Lock Record中的Object reference找到对应的对象,之后让Lock Record中的 ==lock record 地址 00==与Object里的==Hashcode Age Bias 01==进行交换,注意这步交换叫做cas
cas完毕后Object中==lock record 地址 00==记录了Thread0的地址 与 现在Object对象的加锁状态为00 也就是轻量级锁
如果cas操作失败的话,会有两种情况,第一种是其他线程持有了Object对象的轻量级锁 这是说明发生了竞争 进入==锁膨胀==过程,第二种情况是自己执行了synchronized锁重入,那么再加一条Lock Record仅仅作为计数 里面的内容为null 此时的这种情况叫做==锁重入==。
很明显这里的情况是第二种,锁重入。
当退出synchronized代码块时(解锁时) 如果取值为null 说明有重入,这时将重置锁记录,表示重入计数减一
当退出synchronized代码块时(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
如果成功 说明解锁成功
如果失败 说明轻量级锁已经进行了锁膨胀或者已经升级为了重量级锁,进入重量级锁的解锁过程
04.锁膨胀(轻量级锁变为重量级锁)
如果在尝试加轻量级锁的过程中cas操作无法成功,这时一种情况就是有其他线程对此对象已经加上的轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
Thread-1线程
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
// 同步块A
}
}
当Thread1进行轻量级加锁时,Thread0已经对该对象加了轻量级锁,所以此时Thread1加轻量级锁失败,进入了锁膨胀流程
此时Thread-1加锁失败申请变为重量级锁,进入锁膨胀过程,即Object对象申请Monitor锁 让Object指向重量级锁地址,然后自己进入Monitor的EntryList 的阻塞列表 也就是BLOCKED状态
当Thread-0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头失败,因为此时的Object对象中的Mark Word中记录的是Monitor地址,所以这时会进入重量级解锁流程,即按照Monitor地址 找到 Monitor对象 把Object对象的原数据由Thread-0线程存入Monitor里,设置Owner为null,唤醒EntryList中BLOCKED线程
注意:这里的锁膨胀过程 Thread-1进入阻塞列表前其实还有一段自旋过程,也就是后续的自旋锁
05.自旋锁
当我们的轻量级锁在锁膨胀变为重量级锁的过程中,Thread1线程不会直接就进入阻塞列表,它会进入一个循环 然后一直试探Thread0线程是否已经解锁,这样做的目的是 避免进入阻塞状态 减少不必要的上下文切换 增加执行速度。
注意
需要注意的是 自旋锁是指锁膨胀过程中,如果自旋锁过程中成功获取到了cpu 则依然会处于轻量级锁状态,如果自旋过程失败 则会升级为重量级锁
06.偏向锁(对轻量级锁的优化)
偏向锁是对轻量级锁的优化,既然是优化就说明轻量级锁还有缺点,我们来看下面这个例子
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
// 同步块A
method2();
}
}
public static void method2(){
synchronized(obj){
// 同步块B
method3();
}
}
public static void method3(){
synchronized(obj){
// 同步块B
}
}
这个代码我们如果用轻量级锁就会导致锁重入,也就是除了第一次的锁记录替换了对象的MarkWord,剩下的几次锁记录都是null 但是 依然会进行与MarkWord的替换操作 虽然替换失败 但是 替换本身就是一个cas,耗费时间,这就是我们的优化点。
偏向锁如何优化
知道上述轻量级锁的缺点,偏向锁就对症下药,为了避免锁重入的cas操作 所以偏向锁的做法是:第一次调用线程时 用ThreadId替换MarkWord,之后如果再次调用这个线程 只需要检查这个ThreadId是否是自己的,这样就不用进行替换操作避免了不必要的cas
用图片进行对比
轻量级锁
偏向锁
偏向锁状态详解
如何判断是否为偏向锁,是通过MarkWord里的==biased_lock==,如果为0说明不是偏向锁,如果是1说明是偏向锁。
偏向锁的几种情况
1.偏向锁默认延迟一段时间生效,也就是一个线程最开始是normal状态 之后变为 Biased状态2.偏向锁也可以通过jvm设置为立即生效
3.当竞争非常激烈是不建议使用偏向锁,可以通过jvm设置关闭偏向锁
4.当监视器对象 调用hashCode方法后 便禁用了偏向锁,原因需要从MarkWord的结果进行分析 因为normal状态与Biased状态的区别在于normal状态有hashCode站25位 但是hashCode是在调用hashCode方法之后才会出现的,hashCode一旦出现 则占用25位,这样Biased状态的==thread:23 epoch:2==就被占用 所以便不能再使用偏向锁
偏向锁:撤销
情况一
当有另外一个与之竞争相互错开的线程也想用同一个对象的偏向锁时,则会进行锁升级 升级为轻量级锁,注意前提是竞争相互错开 如果产生竞争 则会升级为重量级锁
情况二
当使用wait/notify时也会撤销偏向锁 升级为重量级锁,原因是wait/notify只有在重量级锁时才会使用
批量重偏向
批量重偏向的目的是为了对偏向锁进行优化,优化点在于:虽然有多个线程 但是这多个线程的时间是错开的 那么就会变成轻量级锁 但是会出现这种情况 比如Thread0 创建了30个锁每个锁都执行一遍此时偏向Thread0是 然后 Thread1错开执行了这30个锁 对于每个锁都是由原来的偏向Thread0的锁变为轻量级锁,很明显万一这个锁不止30个 更多呢,那么每一个锁都会经历一次偏向锁变为轻量级锁的过程,所以这个就是优化点。
我们默认进行20次换锁之后 就把偏向锁偏向当前线程
批量撤销
当我们撤销次数达到40次之后(可能是先撤销20次 变为偏向了另一个线程,然后又被其他线程撤销了20次 此时便不会偏向当前线程,而是直接撤销全部),jvm就认为竞争激烈,就把这个类下的全部对象的锁升级为轻量级锁或重量级锁