一、偏向锁简介
Java偏向锁是
Java6
引⼊的⼀项多线程优化。顾名思义,它会
偏向于第⼀个访问锁对象的线
程
,如果同步锁只有⼀个线程访问,则线程是不需要触发同步的,这种情况下,
就会给该线
程加⼀个偏向锁
;如果在运⾏过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁升级到
轻量级锁
,然后再唤醒原持有偏向锁的线程。
二、偏向锁的场景
通过上述简介可以看出,偏向锁的场景非常苛刻,只有在同步块只有一个线程不断访问的情况下才会展现出它的优势,但是在我们编码的大部分场景下都不能满足该要求,所以在Java15中废弃了偏向锁。
三、偏向锁默认启动设置
JDK 1.6 之后默认是开启偏向锁的,为什么初始化的代码是⽆锁状态,进⼊同步块产⽣竞争就绕过偏向锁直接变成轻量级锁了呢?
虽然默认开启了偏向锁,但是开启有延迟,⼤概 4s( 只有项目初始化4s之后新建的对象才会进入可偏向状态 ) 。原因是 JVM 内部的代码有很多地⽅⽤到了synchronized( 不符合偏向锁的场景,不会带来性能的提升,还会造成偏向锁到轻量级锁的升级 ),如果直接开启偏向,产⽣竞争就要有锁升级,会带来额外的性能损耗,所以就有了延迟策略。我们可以通过参数 -XX:BiasedLockingStartupDelay=0 将延迟改为0,但是不建议这么做。
在上述图中描述了不同级别锁之间的升级以及解锁的全部过程,其中锁只能由偏向状态转为非偏向状态,他是单向且不可逆的,锁的状态可细分为五种状态:
- 可偏向状态:jvm开启了偏向锁,该对象是jvm启动4s之后创建的对象,并且还没有线程访问过该同步块。
- 已偏向状态:jvm开启了偏向锁,该对象是jvm启动4s之后创建的对象,只有一个线程访问了该同步块。
- 无锁状态:无锁状态分为两种。第一种是该对象是jvm启动4s之前创建的对象,则直接是无锁状态。第二种是该对象原本处于已偏向状态,并且原持有偏向锁的线程当前没有访问同步块,此时另一个线程访问同步块,会先进入无锁状态(也成为撤销偏向,下文会介绍)。
- 轻量级锁:该对象处于无锁状态,此时有线程竞争会升级为轻量级锁。
- 重量级锁:当对象处于已偏向或者轻量级锁状态时,并且仍持有锁对象(同步块未执行完毕),此时要计算对象的hashcode值,会直接膨胀为重量级锁。
四、不同场景下锁状态转变的代码示例
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
/**
* @author: zhouzhenghu
* @description: 描述synchronized不同场景下锁对象状态的转变
* @date: 2024/6/27 21:05
* @version: 1.0
*/
@Slf4j
public class LockTest {
/**
* @param :
* @return void
* @author ZHOUZH
* @description 偏向4s延迟之前创建锁对象
* 【前提】:偏向延迟4s内创建的对象
* 【变化】:无锁——>轻量级锁
* @date 2024/6/30 15:15
*/
@Test
public void test1() {
Object obj = new Object();
logPrint(obj, "【1】未进入同步块,MarkWord为:"); // non-biasable-无锁:十六进制最后一位的1=0001
synchronized (obj) {
logPrint(obj, "【2】进入同步块,MarkWord为:"); // thin lock-轻量级锁:十六进制最后一位的0=0000
}
}
/**
* @param :
* @return void
* @author ZHOUZH
* @description 超出偏向延迟
* 【前提】:偏向延迟4s后创建的对象
* 【变化】:可偏向状态——>已偏向状态——>已偏向状态(未持有锁)
* @date 2024/6/30 15:21
*/
@Test
@SneakyThrows
public void test2() {
TimeUnit.SECONDS.sleep(5);// 睡眠 5s
Object obj = new Object();
logPrint(obj, "【1】未进入同步块,MarkWord为:"); // biasable-可偏向状态(101 + 没有具体线程):十六进制最后一位的5=0101
synchronized (obj) {
logPrint(obj, "【2】进入同步块,MarkWord为:"); // biased-已偏向状态(101 + 有具体线程):在obj对象的MarkWord中已经存在线程id了
}
logPrint(obj, "【3】跳出同步块,MarkWord为:"); // biased-跳出同步块,还是已偏向状态,即:线程id依然存在
}
/**
* @param :
* @return void
* @author ZHOUZH
* @description 锁升级:偏向锁升级为轻量级锁
* 【前提】:偏向延迟4s后创建的对象
* 【变化】:可偏向状态——>已偏向状态——>已偏向状态(未持有锁)——>轻量级锁——>无锁——>轻量级锁——>无锁
* @date 2024/6/30 15:28
*/
@Test
@SneakyThrows
public void test3() {
TimeUnit.SECONDS.sleep(5); // 睡眠 5s
Object obj = new Object();
logPrint(obj, "【1】未进入同步块,MarkWord为:"); // biasable-可偏向状态(101 + 没有具体线程):十六进制最后一位的5=0101
synchronized (obj){
logPrint(obj, "【2】进入同步块,MarkWord为:"); // biased-已偏向状态(101 + 有具体线程):在obj对象的MarkWord中已经存在线程id了
}
logPrint(obj, "【3】跳出同步块,MarkWord为:"); // biased-跳出同步块,还是已偏向状态,即:线程id依然存在
// 下面这一步有一个隐式的转换,他是先由已偏向状态(未持有锁)转为无锁,再有无锁转为轻量级锁
Thread t2 = new Thread(() -> {
synchronized (obj) {
logPrint(obj, "【4】新线程获取锁,MarkWord为:"); // thin lock-轻量级锁:十六进制最后一位的0=0000
}
});
t2.start();
t2.join();
logPrint(obj, "【5】跳出同步块,MarkWord为:"); // non-biasable-无锁(001 + 没有具体线程):十六进制最后一位的1=0001
synchronized (obj){
logPrint(obj, "【6】主线程再次进入同步块,MarkWord 为:"); // thin lock-轻量级锁:十六进制最后一位的0=0000
}
logPrint(obj, "【7】跳出同步块,MarkWord为:"); // non-biasable-无锁(001 + 没有具体线程):十六进制最后一位的1=0001
}
/**
* @param :
* @return void
* @author ZHOUZH
* @description 【hashcode场景1】:对象处于可偏向状态,没有线程竞争,计算hashcode
* 【前提】:偏向延迟4s后创建的对象
* 【变化】:可偏向状态——>无锁——>轻量级锁
* 【说明】:对象计算hashcode需要地方存储计算的值,偏向状态的对象是没有地方存储hashcode的,所以要转为有地方存储hashcode的无锁状态
* @date 2024/6/30 15:30
*/
@Test
@SneakyThrows
public void test4() {
TimeUnit.SECONDS.sleep(5); // 睡眠 5s
Object obj = new Object();
logPrint(obj, "【1】未生成 hashcode,MarkWord 为:"); // biasable-可偏向状态(101 + 没有具体线程):十六进制最后一位的5=0101
obj.hashCode();
logPrint(obj, "【2】已生成 hashcode,MarkWord 为:"); // 无锁(001 + hashcode值):十六进制最后一位的1=0001
synchronized (obj){
logPrint(obj, "【3】进入同步块,MarkWord 为:"); // thin lock-轻量级锁:十六进制最后一位的0=0000
}
}
/**
* @param :
* @return void
* @author ZHOUZH
* @description 【hashcode场景2】:对象处于已偏向状态,但是没有线程竞争,计算hashcode
* 【前提】:偏向延迟4s后创建的对象
* 【变化】:可偏向状态——>已偏向状态——>无锁——>轻量级锁
* 【说明】:对象计算hashcode需要地方存储计算的值,已偏向状态的对象是没有地方存储hashcode的,所以要转为有地方存储hashcode的无锁状态
* @date 2024/6/30 15:33
*/
@Test
@SneakyThrows
public void test5() {
TimeUnit.SECONDS.sleep(5); // 睡眠 5s
Object obj = new Object();
logPrint(obj, "【1】未生成 hashcode,MarkWord 为:"); // biasable-可偏向状态(101 + 没有具体线程):十六进制最后一位的5=0101
synchronized (obj) {
logPrint(obj, "【2】进入同步块,MarkWord 为:"); // biased-已偏向状态(101 + 有具体线程):在obj对象的MarkWord中已经存在线程id了
}
obj.hashCode();
logPrint(obj, "【3】生成 hashcode"); // 无锁状态
synchronized (obj) {
logPrint(obj, "【4】同一线程再次进入同步块,MarkWord 为:"); // thin lock-轻量级锁:十六进制最后一位的8=1000
}
}
/**
* @param :
* @return void
* @author ZHOUZH
* @description 【hashcode场景3】:对象处于已偏向状态,并且持有锁,计算hashcode
* 【前提】:偏向延迟4s后创建的对象
* 【变化】:可偏向状态——>已偏向状态——>重量级锁
* 【说明】:对象计算hashcode需要地方存储计算的值,已偏向状态的对象是没有地方存储hashcode的,
* 又因为此时正持有偏向锁,所以要转为有地方存储hashcode的重量级锁,
* 重量级锁指针所指向的对象(ObjectMonitor类)中有地方可以记录无锁状态下(标志位为“01”)下的markword,
* 其中自然可以存储hashcode值
* @date 2024/6/30 15:35
*/
@Test
@SneakyThrows
public void test6() {
TimeUnit.SECONDS.sleep(5); // 睡眠 5s
Object obj = new Object();
logPrint(obj, "【1】未生成 hashcode,MarkWord 为:"); // biasable-可偏向状态(101 + 没有具体线程):十六进制最后一位的5=0101
synchronized (obj){
logPrint(obj,"【2】进入同步块,MarkWord 为:"); // biased-已偏向状态(101 + 有具体线程):在obj对象的MarkWord中已经存在线程id了
obj.hashCode();
logPrint(obj,"【3】已偏向状态下,生成 hashcode,MarkWord 为:"); // fat lock-重量级锁:十六进制最后一位的a=1010
}
}
/**
* @param :
* @return void
* @author ZHOUZH
* @description 【hashcode场景4】假如对象处于轻量级锁的状态,并且持有锁,计算hashcode
* 【前提】:偏向延迟4s内创建的对象
* 【变化】:无锁——>轻量级锁——>重量级锁
* 【结论】:如果对象处在轻量级锁的状态,生成 hashcode 后,就会直接升级成重量级锁
* @date 2024/6/30 15:45
*/
@Test
@SneakyThrows
public void test7() {
Object obj = new Object();
logPrint(obj, "【1】未进入同步块,MarkWord为:"); // non-biasable-无锁:十六进制最后一位的1=0001
synchronized(obj) {
logPrint(obj, "【2】进入同步块,MarkWord为:"); // thin lock-轻量级锁:十六进制最后一位的0=0000
obj.hashCode();
logPrint(obj,"【3】已偏向状态下,生成 hashcode,MarkWord 为:"); // fat lock-重量级锁:十六进制最后一位的a=1010
}
}
public void logPrint(Object obj, String info) {
log.info(info);
log.info(ClassLayout.parseInstance(obj).toPrintable());
}
}
上图中更加详细地描述了代码示例中不同case下状态流转的过程。并且代码方法备注中都有详细描述。