偏向锁是什么?
是jdk1.6引入的一种锁优化方式。让 锁对象 偏心于第一次获取锁的线程,记住它的id,当下一次再有线程获取锁的时候,与记录的ID匹配,直接获取锁就行。是一种load-and-test
的过程。
引入目的?
基础库中很多类为了线程安全,添加了synchronized
进行同步,保证线程安全。很多时候,我们在使用这些类的对象时,都是单线程进行访问。轻量级锁的获取锁和释放锁涉及多次CAS操作,而偏向锁只依赖一次CAS。比轻量级锁更加轻量。
JDK1.6默认开启,JDK15后默认关闭,可以通过-XX:-UseBiasedLocking
进行配置启用还是禁用。
优点
- 加锁解锁无需额外的消耗,和非同步方法时间相差纳秒级别
缺点
- 如果存在多个线程同时访问该锁对象,则会带来额外的锁撤销的消耗。
对象状态
以下状态默认为开启偏向锁配置
对象创建
当开启偏向锁时,类的prototype_header
中的锁标志位为01
,是否可偏向locked_bias
为1
,thread id为0
,表示匿名偏向(anonymously biased)。
出于性能的考虑,在JVM启动后的头4秒,偏向锁配置是关闭的,locked_bias
为0
,以禁止实例化对象被偏向。4秒后,prototype_header.locked_bias
会重新设置为1
,新的对象就可以被偏向绑定了。
可通过-XX:BiasedLockingStartupDelay=0
将延迟改为0。即偏向锁设置在JVM一启动就生效。
锁状态
未绑定不可偏向
此时locked_bias
为0,如果有线程获取此锁,会升级为轻量级锁,如果有多个线程尝试获取此锁,会升级成重量级锁。
什么时候会变成此状态呢?
- 计算对象的
hashCode
,调用Object.hashCode
或System.identityHashCode(Object)
-
- 对象的
hashCode
并不是一创建就计算好的,而是在调用hashCode
方法后,才存储在对象头中的。 - 一旦对象进行
hashCode
计算,都会从可偏向状态转为此状态。如果正在偏向,则会发生锁撤销操作。
- 对象的
- 偏向锁被禁用
-
- jvm参数
-XX:-UseBiasedLocking
将偏向锁禁用 - jvm启动头4秒
- 发生批量锁撤销后
- jvm参数
-
-
- 当该类所有对象发生锁撤销的次数达到40次后,会触发批量锁撤销。将
prototype_header
中的locked_bias
设置为0,并对所有该类对象触发锁撤销操作。以后新创建的对象,都是不可偏向的状态。
- 当该类所有对象发生锁撤销的次数达到40次后,会触发批量锁撤销。将
-
- 偏向锁被撤销
-
-
当锁对象已偏向某个线程,此时有另外一个线程来访问该锁对象,且不可重偏向则会发生锁撤销。
-
批量锁撤销时
-
可偏向
locked_bias
为1
匿名偏向
当Mark Word
中的Thread ID
偏向线程为0
时,为匿名偏向。
意味着该锁还没有偏向于任何线程。第一个试图获得锁的线程,使用CAS
指令就可将线程绑定在此锁对象上。
这是允许偏向对象的初始状态。
已偏向
Thread ID
不为空,且对象头的Mark Word.epoch
与Klass._prototype_header.epoch
相等。表示当前锁已偏向于某个线程,并且不可重偏向。如果当前线程不是锁对象偏向的线程,则触发锁撤销。升级成轻量级锁。
可重偏向
Thread ID
不为空,但是对象头的Mark Word.epoch
与Klass._prototype_header.epoch
不相等。代表当前锁对象的偏向已过期,可进行重偏向。使用CAS
将新线程绑定于当前锁对象。
加锁过程
- 验证
Mark Word.locked_bias
位
-
- 为
0
,不可偏向,升级为轻量级锁
- 为
- 验证
Klass.prototype_header.locked_bias
位
-
- 为
0
,则该类所有对象全部不允许偏向,需要重置该类所有对象的locked_bias
位,该类所有对象Mark Word
替换为无锁标识,当前对象升级为轻量级锁。
- 为
- 比较对象和原型
epoch
位,即Mark Word.epoch == Klass._prototype_header.epoch
-
- 不想等,代表该锁的偏向已过期,可进行重偏向。进行重偏向获取偏向锁。
- 相等,代表该锁已偏向某个线程。继续第4步重入或者升级为轻量级锁
- 检验
owner
线程
比较偏向线程id和当前线程id
-
- 匹配,进行重入锁,向当前线程栈中写入一条
Displaced Mark Word
为空,Owner
指向锁对象的Lock Record
。 - 不匹配,使用CAS进行线程替换(0->当前线程)。如果替换失败,需要进行线程撤销,替换
Mark Word
为无锁状态,并升级为轻量级锁。根据偏向线程是否还在执行同步代码块,判断是哪个线程获取升级后的轻量级锁。
- 匹配,进行重入锁,向当前线程栈中写入一条
-
-
- 偏向线程正在执行同步代码块,则偏向线程获取该锁。
- 偏向线程未处于活动状态或退出同步代码块,则当前线程获取轻量级锁。
-
解锁过程
偏向锁加锁,会向获取到锁的线程栈中插入一条Lock Record
,其中的属性Displaced Mark Word
为null,Owner
指向当前锁对象。
偏向锁的解锁,是将线程栈中的Lock Record.Owner
设置为null
即为解锁。
锁撤销
- 将
Mark word
设置为无锁状态,并升级为轻量级锁。
-
- 如果偏向线程还在执行同步代码块,则偏向线程获取该轻量级锁,当前线程进行轻量级锁升级重量级锁的自旋等待。
- 如果偏向线程已执行完同步代码块或未处于活动状态,则当前线程获取升级后的轻量级锁。
批量重偏向
bulk rebias
避免短时间内大量偏向锁的撤销。例如一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。当执行批量重偏向后,如果原偏向锁持有者线程不再执行同步块,则锁可以偏向于新线程。
在Klass
中维护了一个偏向锁撤销计数器revocation_count
,每次该类的对象发生偏向锁撤销操作时,该计数器+1,当达到阈值20时,JVM会认为该偏向锁存在问题,给该偏向锁一个重偏向的机会。将Klass.prototype_header.epoch
+1。并会对该类的所有对象的偏向状态进行初始化。
- 如果该对象的偏向线程当前持有该锁对象,则更新
mark word.epoch
=_prototype_header.epoch
,表示该锁对象已偏向。 - 其他对象不更新,表示该对象偏向已过期,可进行重新偏向。
批量锁撤销
当revocation_count
达到阈值40时,JVM认为该偏向锁,存在多线程竞争关系。
但是在彻底禁用偏向锁之前,JVM还给了一次改过自新的机会,存在另外一个计时器BiasedLockingDecayTime = 25000
- 如果在距离上次批量重偏向发生的 25 秒之内,并且累计撤销计数达到40,就会发生批量撤销(偏向锁彻底 game over)
- 如果在距离上次批量重偏向发生超过 25 秒之外,那么就会重置在
[20,40)
内的计数, 再给次机会。
禁用偏向锁过程:更新klass._prototype_header.locked_bias
为0,之后对于该class对象的锁,直接走轻量级锁逻辑。
如何查询Mark Word
- openjdk提供了一个库查看内存布局工具
JOL(Java object layout)
JOL maven 仓库地址
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
复制代码
- 其中
ClassLayout.parseInstance(o).toPrintable()
可以打印出Mark word
信息。查看锁状态。
示例
package com.learn.project;
import org.openjdk.jol.info.ClassLayout;
/**
* MarkWordTest
*
* @author chenfuyuan
* @date 2022/12/10 19:01
*/
public class MarkWordTest {
public static void main(String[] args) throws InterruptedException {
User initBeforeBiasSetting = new User();
System.out.println("设置可偏向之前:"+ printClassLayout(initBeforeBiasSetting));
Thread.sleep(5000L);
User initAfterBiasSetting = new User();
System.out.println("设置可偏向之后:"+ printClassLayout(initAfterBiasSetting));
synchronized (initAfterBiasSetting) {
//进入同步代码块
System.out.println("进入同步代码块,进行偏向绑定:" + printClassLayout(initAfterBiasSetting));
}
Thread thread01 = new Thread(() -> {
synchronized (initAfterBiasSetting) {
System.out.println("被另外一个线程调用后(thread):" + printClassLayout(initAfterBiasSetting));
}
});
thread01.start();
Thread.sleep(1000L);
System.out.println("被另外一个线程调用后(main):" + printClassLayout(initAfterBiasSetting));
}
private static String printClassLayout(Object obj) {
return ClassLayout.parseInstance(obj).toPrintable();
}
}
===================输出=================
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/bin/java -javaagent:/Users/chenfuyuan/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.7571.182/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=58794:/Users/chenfuyuan/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.7571.182/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/rt.jar:/Users/chenfuyuan/code/learn/java/learn-jdk/learn-juc/target/classes:/Users/chenfuyuan/resource/maven_repository/learn/org/openjdk/jol/jol-core/0.16/jol-core-0.16.jar com.learn.project.MarkWordTest
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
设置可偏向之前:com.learn.project.User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c143
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
设置可偏向之后:com.learn.project.User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf800c143
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
进入同步代码块,进行偏向绑定:com.learn.project.User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fe28f809005 (biased: 0x0000001ff8a3e024; epoch: 0; age: 0)
8 4 (object header: class) 0xf800c143
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
被另外一个线程调用后(thread):com.learn.project.User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000003061f89e8 (thin lock: 0x00000003061f89e8)
8 4 (object header: class) 0xf800c143
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
被另外一个线程调用后(main):com.learn.project.User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c143
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Process finished with exit code 0
复制代码
注意事项
- 必须调用Object.hashCode才会进行锁撤销。重载的
hashCode
方法无法触发锁撤销。 - 在同步代码块中调用
hashCode
,锁对象会直接升级成重量级锁。
待补充的知识
- 轻量级锁
- 重量级锁
- 安全点 safe point