文章目录
- 1、偏向锁出现的背景
- 2、从共享对象的内存结构看偏向锁
- 3、偏向锁的持有
- 4、启动偏向锁
- 5、sleep暂停来启动偏向锁
- 6、偏向锁的撤销
- 7、总体流程
- 8、SinceJava15 偏向锁的废除
1、偏向锁出现的背景
如果一个线程连续几次抢到锁,仍然重复加锁解锁,就会导致用户态和内核态频繁切换,这显然是有改进空间的。如之前买票的例子:
public class SaleTick {public static void main(String[] args) {Ticket ticket = new Ticket();new Thread(() -> {for (int i = 0; i < 50; i++) {ticket.sale();}},"t1").start();new Thread(() -> {for (int i = 0; i < 50; i++) {ticket.sale();}},"t2").start();new Thread(() -> {for (int i = 0; i < 50; i++) {ticket.sale();}},"t3").start();}
}//资源类
class Ticket {private int number = 50;Object lockObject = new Object();public void sale() {synchronized (lockObject) {if (number > 0) {System.out.println(Thread.currentThread().getName() + "卖出票,剩余票数" + number--);}}}
}
发现一个线程一直在抢到锁:
Hotspot 的作者发现,大多数情况下:多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一个线程多次获得的情况,偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步代码块时提高性能。
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,懒的连CAS操作都不做了,直接提高程序性能。
举个例子:生活中,第一次去店铺A吃牛肉汤,老板会问你的口味,然后接下来几天,天天都去吃这家店,那老板以后看到来的是你,就不会再问了,直接给你按口味做就是了。
2、从共享对象的内存结构看偏向锁
从对象结构来看,偏向锁时,被锁对象请求头Mark word的前54位都存当前线程的指针,末尾的三位则改成了101,即代表偏向锁。
3、偏向锁的持有
当线程A第一次竞争到对象锁时,修改共享对象Mark Word里的偏向线程ID,在没有其他线程竞争的情况下,后续这个线程再进入这个同步代码块时,不需要再次加锁解锁,只需判断对象Mark Word里的ID是不是指向自己:
- 是,就直接执行,且直到有其他线程过来发生竞争才释放锁
- 不是,说明发生了竞争,就尝试通过CAS修改Mark Word里的线程ID为自身ID
上面CAS时:
- 如果修改成功,说明线程B来改时,之前偏向的线程A刚结束,此时,仍为偏向锁,偏向B
- 如果修改失败,升级轻量锁,保证所有线程重新公平竞争
注意点:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的
偏向锁的操作不会直接捅到操作系统,不涉及用户到内核来回转换。以自定义的Account对象的对象头为例:
此时,线程A执行到synchronized同步代码块时,JVM通过CAS操作把线程指针ID记录到Account对象的Mark word 中,并修改偏向标识,线程A获得锁成功。注意,执行完同步代码块后,锁并未释放,等线程A二次来时,JVM判断account的Mark Word里面是否还有线程A的ID,有,就继续执行,因为之前没有释放锁,这里自然不用重新获取锁,也就不涉及用户态和内核态的来回切换。
4、启动偏向锁
终端执行以下,查看偏向锁的配置信息:
java -XX:+PrintFlagsInitial | grep BiasedLock*
可以看到偏向锁默认打开,以及启动偏向锁的延迟时长(默认延迟4秒,我这里JDK版本较高,不是4)
写实例Demo:
可以看到只有一个线程在操作对象o ⇒ 应该是偏向锁 ⇒ 却发现是轻量锁000
这是因为偏向锁延时4秒开启,期间自然是下一级:轻量锁。
偏向锁在JDK1.6之后就默认开启,但启动时间有延迟,想立刻启动,可通过添加JVM参数将延迟改为0:
- -XX:+UseBiasedLocking 开启偏向锁
- -XX:-UseBiasedLocking 关闭偏向锁,此时会直接跳入轻量锁
- -XX:BiasedLockingStartupDelay=0 关闭延迟
添加JVM参数,这里关闭延时,正常显示101,即偏向锁:
5、sleep暂停来启动偏向锁
除了以上添加JVM参数关闭延时来立刻启动偏向锁,也可通过另一种方式:程序执行前等4秒,以保证开启了偏向锁
再对比下,偏向锁开启后,使用synchronized锁时的对象o和不使用synchronized时的对象o的区别:
可以看到二者锁状态均为101,但前面o对象未使用synchronized锁,所以线程ID为空,而后者则带了线程ID。
6、偏向锁的撤销
共享对象o的Mark Word一直指向线程A的ID,线程A也一直拿着这个对象锁。直到第二个线程开始来抢夺锁时,线程A的好日子结束:
-
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。
-
且撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行(客人还在你店里吃饭,你总不能一到打烊时机就掀桌子)
-
如果第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级(升级为轻量锁),且此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
-
第一个线程执行刚好完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。直白说就是,另一线程t2来竞争时,偏向的线程t1刚好执行完,那大家就重新竞争。当然,也有可能t1出代码块后,run方法结束,直接走了,那就偏向t2就行
7、总体流程
8、SinceJava15 偏向锁的废除
JDK15:Disable and Deprecate Biased Locking.
//2020.9.15
Prior to JDK 15, biased locking is always enabled and available. With this JEP, biased locking will no longer be enabled when HotSpot is started unless -XX:+UseBiasedLocking is set on the command line.