synchronized
一、概述
- synchronized的作用:
- 保证在同一时刻,只有一个线程可以执行某个方法或某个代码块。
- 保证
原子性
和可见性
- 同步的前提:
- 两个或者两个以上的线程
- 多个线程使用同一个锁
二、synchronized的使用
1、同步代码块
需要显示指定锁对象(可以是任意对象
),进入同步代码块
前,要先获取指定的锁对象
public class SynchronizedCodeBlock {
static int number = 100;
public static void main(String[] args) {
Object lock = new Object();
new Thread(()-> {
// 同步代码块开始,获取锁(lock对象)
synchronized (lock) {
number--;
}
// 同步代码块结束,释放锁(lock对象)
}).start();
}
}
2、同步方法(非静态)
不需要指定锁对象(锁是 当前实例对象
,即 this
),进入 同步方法
前,要先获取对应的锁对象。
public class SynchronizedMethod {
static int number = 100;
public static void main(String[] args) {
SynchronizedMethod synchronizedMethod = new SynchronizedMethod();
// 同步方法开始,获取锁(synchronizedMethod对象)
synchronizedMethod.syncMethod();
// 同步方法结束,释放锁(synchronizedMethod对象)
}
public synchronized void syncMethod() {
number--;
}
}
3、同步方法(静态)
不需要指定锁对象(锁是 当前类的Class对象
),进入 静态同步方法
前,要先获取对应的锁对象。
public class StaticSynchronizedMethod {
static int number = 100;
public static void main(String[] args) {
// 静态同步方法开始,获取锁(StaticSynchronizedMethod类)
StaticSynchronizedMethod.staticSyncMethod();
// 静态同步方法结束,释放锁(StaticSynchronizedMethod类)
}
public static synchronized void syncMethod() {
number--;
}
}
三、Monitor 监视器
1、Monitor 是什么
Monitor 称作 监视器/管程,是由操作系统提供的对象。
- 每个Java对象都可以关联一个Monitor
- 如果用
synchronized
给对象上锁(重量级锁),该对象头的Mark Word
中就会记录指向Monitor对象的指针。
2、工作原理
关于 Owner
:
- 每个锁对象都会关联一个Monitor,一个Monitor同一时刻只能有一个
Owner
。 - 刚开始时,Monitor 中的
Owner
为null
- 线程开始执行 synchronized 代码块/方法,如果获取锁成功,则将
Owner
设置为获取锁的线程
。 - 线程执行完 synchronized 代码块/方法,释放锁,则将
Owner
设置为null
。
关于 EntryList
:
- 竞争锁失败的线程,会变成
BLOCKED
状态,放到EntryList
中。 - 占据锁的线程释放锁后,
EntryList
中BLOCKED
的线程就会被唤醒来竞争锁(竞争是非公平的)
关于 WaitSet
:
-
获取了锁之后进入
WAITING
状态的线程,会放到WaitSet
中。例如:Owner 线程调用 wait 方法,变为
WAITING
状态,进入WaitSet
-
WaitSet
中WAITING
状态的线程被唤醒后,可以进入EntryList 重新竞争锁(注意:这里唤醒后不是立即获取锁!)例如:其他线程 调用 notify 或 notifyAll 唤醒
WaitSet
中WAITING
状态的线程
注意:
- 不加 synchronized,不会关联监视器,不遵从以上规则
- 加了 synchronized,必须同一个锁对象的 Monitor 才有上述的效果
3、字节码演示
JVM是通过 进入对象监视器(
monitorenter
) 和 退出对象监视器(monitorexit
)来实现对方法、同步块的同步的。
- 在 同步方法调用之前 加入一个
monitorenter
指令- 在 退出方法和异常处 加入一个
monitorexit
指令。
本质就是获取对象监视器Monitor,而这个获取过程具有排他性,从而达到了同一时刻只能一个线程访问的目的。
没有获取到锁的线程,将会阻塞在方法的入口处,直到获取锁的线程 monitorexit
后才能尝试继续获取锁。
public class MonitorDemo {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
对应的字节码(命令:javap -p -c MonitorDemo.class
)
public class thread.MonitorDemo {
// ....
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // 获取lock引用(synchronized开始)
3: dup // 复制lock引用
4: astore_1 // 存储lock引用 -> slot 1
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
/* synchronized代码块内容 */
6: getstatic #3 // <- counter
9: iconst_1 // 准备常数0
10: iadd // +1
11: putstatic #3 // -> counter
14: aload_1 // 载入lock引用 <- slot 1
15: monitorexit // 将 lock对象 MarkWord 重置,唤醒 EntryList
16: goto 24 // 跳转到24行
/* 异常处理指令(出现异常,也能自动地释放锁) */
19: astore_2 // 存储异常e -> slot 2
20: aload_1 // 载入lock引用 <- slot 1
21: monitorexit // 将 lock对象 MarkWord 重置,唤醒 EntryList
22: aload_2 // 载入异常e <- slot 2
23: athrow // throw e
24: return
/* 异常处理 */
Exception table:
from to target type
6 16 19 any // 6~16行出现异常,跳转到19行
19 22 19 any
// ....
}
注意:方法级别的 synchronized 不会在字节码指令中有所体现
四、synchronized 锁分类
Java对象头
的 Mark Work
中包含了 锁
的信息(下面是以32位虚拟机为例,64位虚拟机也差不多)
1、重量级锁
Monitor 监视器
就属于重量级锁
重量级锁的 Mark Work:
ptr_to_heavyweight_monitor
:指向重量级锁的指针10
:锁标志位,表示是重量级锁
2、轻量级锁
如果一个对象虽然有多个线程访问,但是多线程访问的时间是错开的(也就是没有竞争),可以使用轻量级锁
优化。
轻量级锁
对使用者是透明的(即语法仍然是synchronized
)- 如果有锁竞争,
轻量级锁
会升级为重量级锁
3、偏向锁
Tips:关于偏向锁的说明可能会不太理解,为了结构清晰先提一嘴,先知道有这么个东西,后面有详细说。
轻量级锁
在每次重入时,CAS都会失败,但是还是无法避免进行CAS。
Java 6 中引入了偏向锁
进行优化:
- 第一次进行 CAS 时,将
thread
设置到 对象头的 Mark Word 中。 - 之后每次获取锁,只要
thread
和 Mark Word 中的一致,就表示没有竞争,不用再次 CAS。
五、加锁流程分析
这里主要分析的是 轻量级锁 的加锁流程
1、加锁流程
执行 synchronized 代码块/方法时,会在线程的栈帧中创建轻量级锁记录 Lock Record
:
lock record
地址00
:锁标志位,表示是轻量级锁Object reference
:用于存储锁对象的引用
然后做两件事
Object reference
指向锁对象 Object
- 采用 CAS 替换
lock record地址 + 锁标志位00
和锁对象 Object 的 Mark Word
CAS 替换成功,如下图所示
CAS 替换失败,有两种情况:
- 情况1:其它线程已经持有了
Object
的轻量级锁
(锁竞争),进入锁膨胀
过程。 - 情况2:自己已经持有了
Object
的轻量级锁
(重入),添加一条锁信息为 null 的 Lock Record,作为重入的计数(如下图)
synchronized 代码块/方法结束,如果有锁信息为 null 的 Lock Record(有重入),优先清除该Lock Record,重入计数减1
如果没有锁信息为 null 的 Lock Record(没有重入),则用 CAS 将 Mark Word 的值恢复给 锁对象 Object
- 成功,则解锁成功(00 轻量级锁 —> 01 无锁)
- 失败,则说明
轻量级锁
已经升级为重量级锁
,进入重量级锁的解锁流程。
2、锁膨胀(轻量级锁 -> 重量级锁)
加锁流程中,如果其他线程已经持有了
轻量级锁
(锁竞争),会导致CAS替换失败
- 此时就要进行
锁膨胀
:将轻量级锁
升级为重量级锁
。
当 Thread-1 获取轻量级锁
时,发现 Thread-0 已经持有了轻量级锁
此时发生了锁竞争,Thread-1 加轻量级锁
失败,进入锁膨胀
流程:
-
锁对象 Object
由轻量级锁
升级为重量级锁
lock record地址 + 锁标志位00
—>Monitor地址 + 锁标志位10
-
Monitor 的
Owner
指向 原来已经持有轻量级锁
的 Thread-0 -
Thread-1 变成
BLOCKED
状态,进入 Monitor 的EntryList
当 Thread-0 结束 synchronized 同步块/方法,发现没有重入,则用 CAS 将 Mark Word 的值恢复给 锁对象 Object
此时,由于锁膨胀
, 锁对象 Object
已经从 轻量级锁
升级为 重量级锁
,所以 CAS 失败,进入重量级锁
的解锁流程。
3、重量级锁的解锁流程
- 根据 Monitor的地址,找到 Monitor 对象
- 将 Owner 设置为 null
- 唤醒 EntryList 中 BLOCKED 的 线程
4、自旋的优化
竞争重量级锁
的时候,可以使用自旋来进行优化
- 如果当前线程自旋成功(即在自旋时持锁的线程释放了锁),那么当前线程就可以避免阻塞(不用进行上下文切换)就获得了锁
自旋重试成功的情况
线程 1 (core 1 上) | 锁标志位 | 线程 2 (core 2 上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取 monitor | 10(重量锁) | - |
成功(加锁) | 10(重量锁) | - |
执行同步块 | 10(重量锁) | - |
执行同步块 | 10(重量锁) | 访问同步块,获取 monitor |
执行同步块 | 10(重量锁) | 自旋重试 |
执行完毕 | 10(重量锁) | 自旋重试 |
成功(解锁) | 01(无锁) | 自旋重试 |
- | 10(重量锁) | 成功(加锁) |
- | 10(重量锁) | 执行同步块 |
- | … | … |
自旋重试失败的情况:自旋了一定次数,还是没有等到持锁的线程释放锁
线程 1(core 1 上) | 锁标志位 | 线程 2(core 2 上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取 monitor | 10(重量锁) | - |
成功(加锁) | 10(重量锁) | - |
执行同步块 | 10(重量锁) | - |
执行同步块 | 10(重量锁) | 访问同步块,获取 monitor |
执行同步块 | 10(重量锁) | 自旋重试 |
执行同步块 | 10(重量锁) | 自旋重试 |
执行同步块 | 10(重量锁) | 自旋重试 |
执行同步块 | 10(重量锁) | 阻塞 |
… | … | … |
注意事项:
-
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
-
Java 6 之后自旋锁是自适应的:
如果上一次自旋操作成功过,那么认为这一次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋。
-
Java 7 之后不能控制是否开启自旋功能
5、锁消除
动态编译同步块时,JIT 编译器可以借助逃逸分析,判断
synchronized
锁对象是不是只可能被一个线程加锁,不存在其他线程来竞争加锁的情况。如果是,JIT 编译器在编译的时候就会取消对这部分代码的同步,这个过程就叫同步省略,也叫锁消除。
- 参数
-XX:+EliminateLocks
:开启同步替换(默认打开)。
线程同步的代价是相当高的,同步的后果是降低并发性和性能。同步省略可以大大提高并发性和性能。
public void eliminateLocks() {
Object obj = new Object();
synchronized(obj) {
System.out.println(obj);
}
}
上述代码中,对 obj 对象加锁,但是obj 对象的生命周期只在方法内,不会逃逸,所以在 JIT 编译阶段就会把锁优化掉,如下所示:
public void eliminateLocks() {
Object obj = new Object();
System.out.println(obj);
}
6、锁粗化
JIT编译时,发现一段代码中对相同对象
多次加锁,导致线程发生多次重入
,会合并为一个锁,避免频繁加锁释放锁。如:
Object obj = new Object();
synchronized(obj) {逻辑1;}
synchronized(obj) {逻辑2;}
synchronized(obj) {逻辑3;}
Object obj = new Object();
synchronized(obj) {
逻辑1;
逻辑2;
逻辑3;
}
六、偏向锁
1、基本概念
轻量级锁
在每次重入时,CAS都会失败,但是还是无法避免进行CAS。
Java 6 中引入了偏向锁
进行优化:
- 第一次进行 CAS 时,将
thread
设置到 对象头的 Mark Word 中。 - 之后每次获取锁,只要
thread
和 Mark Word 中的一致,就表示没有竞争,不用再次 CAS。
注意:这里偏向锁
中的 ThreadId
和 getId()
获取的 线程ID
不一样。
2、偏向锁的Mark Word
3、开启与延迟
偏向锁默认是开启的
- 对象创建后,Mark Word 最后3位为 1 01,这时 thread、epoch、age 都是0。(加锁时才会设置值)
偏向锁默认是延迟的(大概4秒),不会在程序启动的时候立刻生效
- 可以添加JVM参数
-XX:BiasedLockingStartupDelay=0
来禁用延迟
可以引入以下maven依赖后打印观察一下
<!-- 打印 Java 对象内存布局 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
默认情况(开启 + 延迟)
public class BiasedLockDelayTest {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable()); // 0 01 无锁
TimeUnit.SECONDS.sleep(4);
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable()); // 1 01 偏向锁
}
}
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) // 0 01 无锁
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0) // 1 01 偏向锁
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
禁用延迟(开启 + 无延迟)
/**
* VM options:-XX:BiasedLockingStartupDelay=0
*/
public class BiasedLockNoDelayTest {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable()); // 1 01 偏向锁
}
}
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0) // 1 01 偏向锁
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
4、禁用
可以通过 -XX:-UseBiasedLocking
禁用偏向锁
- 对象创建后,Mark Word 最后三位的值是 0 01,这时 hashcode、age 都是0。
- 禁用
偏向锁
后,优先使用轻量级锁
示例:禁用偏向锁 + 禁用延迟
/**
* VM options:-XX:BiasedLockingStartupDelay=0 -XX:-UseBiasedLocking
*/
public class DisableBiasedLockTest {
public static void main(String[] args) {
Object obj = new Object();
ClassLayout classLayout = ClassLayout.parseInstance(obj);
new Thread(() -> {
System.out.println("synchronized 前:");
System.out.println(classLayout.toPrintable());
synchronized (obj) {
System.out.println("synchronized 中:");
System.out.println(classLayout.toPrintable());
}
System.out.println("synchronized 后:");
System.out.println(classLayout.toPrintable());
}).start();
}
}
synchronized 前:
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) // 0 01 无锁
synchronized 中:
0 8 (object header: mark) 0x000000017007a950 (thin lock: 0x000000017007a950) // 00 轻量级锁
synchronized 后:
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) // 0 01 无锁
5、撤销 - 调用对象hashCode
调用对象的hashcode方法
时,会撤销这个对象的偏向状态
- 原因:
偏向锁
的 Mark Word 存储的是thread
,没有位置存储hashcode
的值了
为什么轻量级锁和重量级锁没有这个问题?
轻量级锁
:会在 Lock Record 中存储 hashCode重量级锁
:会在 Monitor 中存储 hashCode
【案例】
调用hashCode之前
/**
* VM options:-XX:BiasedLockingStartupDelay=0
*/
public class RevokeBiasedLockTest1 {
public static void main(String[] args) {
Object obj = new Object();
ClassLayout classLayout = ClassLayout.parseInstance(obj);
new Thread(() -> {
System.out.println("synchronized 前:");
System.out.println(classLayout.toPrintable()); // 1 01 偏向锁
synchronized (obj) {
System.out.println("synchronized 中:");
System.out.println(classLayout.toPrintable()); // 1 01 偏向锁
}
System.out.println("synchronized 后:");
System.out.println(classLayout.toPrintable()); // 1 01 偏向锁
}).start();
}
}
调用hashCode之后,偏向锁
被撤销,升级为轻量级锁
/**
* VM options:-XX:BiasedLockingStartupDelay=0
*/
public class RevokeBiasedLockTest1 {
public static void main(String[] args) {
Object obj = new Object();
ClassLayout classLayout = ClassLayout.parseInstance(obj);
new Thread(() -> {
obj.hashCode(); // 使用对象的hashCode
System.out.println("synchronized 前:");
System.out.println(classLayout.toPrintable()); // 0 01 无锁
synchronized (obj) {
System.out.println("synchronized 中:");
System.out.println(classLayout.toPrintable()); // 00 轻量级锁
}
System.out.println("synchronized 后:");
System.out.println(classLayout.toPrintable()); // 0 01 无锁
}).start();
}
}
6、撤销 - 其他线程使用锁对象
其他线程使用锁对象,偏向锁
被撤销,升级为轻量级锁
/**
* VM options:-XX:BiasedLockingStartupDelay=0
*/
public class RevokeBiasedLockTest2 {
public static void main(String[] args) {
Object obj = new Object();
ClassLayout classLayout = ClassLayout.parseInstance(obj);
new Thread(() -> {
System.out.println("t1线程执行了:");
System.out.println("synchronized 前:");
System.out.println(classLayout.toPrintable()); // 1 01 偏向锁
synchronized (obj) {
System.out.println("synchronized 中:");
System.out.println(classLayout.toPrintable()); // 1 01 偏向锁
}
System.out.println("synchronized 后:");
System.out.println(classLayout.toPrintable()); // 1 01 偏向锁
synchronized (RevokeBiasedLockTest2.class) {
RevokeBiasedLockTest2.class.notify();
}
}, "t1").start();
new Thread(() -> {
// 等t1线程执行完再执行
synchronized (RevokeBiasedLockTest2.class) {
try {
RevokeBiasedLockTest2.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t2线程执行了");
System.out.println("synchronized 前:");
System.out.println(classLayout.toPrintable()); // 1 01 偏向锁
// 这里t2线程使用了相同的锁对象,「偏向锁」撤销,升级为「轻量级锁」
synchronized (obj) {
System.out.println("synchronized 中:");
System.out.println(classLayout.toPrintable()); // 00 轻量级锁
}
System.out.println("synchronized 后:");
System.out.println(classLayout.toPrintable()); // 0 01 无锁
}, "t2").start();
}
}
7、撤销 - 使用wait/notify
使用wait/notify
(涉及到Monitor),偏向锁
被撤销,升级为重量级锁
/**
* VM options:-XX:BiasedLockingStartupDelay=0
*/
public class RevokeBiasedLockTest3 {
public static void main(String[] args) {
Object obj = new Object();
ClassLayout classLayout = ClassLayout.parseInstance(obj);
new Thread(() -> {
System.out.println("t1线程执行了:");
System.out.println("synchronized 前:");
System.out.println(classLayout.toPrintable()); // 1 01 偏向锁
synchronized (obj) {
System.out.println("synchronized 中1:");
System.out.println(classLayout.toPrintable()); // 1 01 偏向锁
try {
obj.wait(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("synchronized 中2:");
System.out.println(classLayout.toPrintable()); // 10 重量级锁
}
System.out.println("synchronized 后:");
System.out.println(classLayout.toPrintable()); // 10 重量级锁
}, "t1").start();
}
}
synchronized 前:
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
synchronized 中1:
0 8 (object header: mark) 0x00000001558be805 (biased: 0x00000000005562fa; epoch: 0; age: 0)
synchronized 中2:
0 8 (object header: mark) 0x0000000155833812 (fat lock: 0x0000000155833812)
synchronized 后:
0 8 (object header: mark) 0x0000000155833812 (fat lock: 0x0000000155833812)
8、批量重偏向
-
重偏向的概念:
撤销是很消耗性能的,而
重偏向
是对撤销
对一个优化。之前已经举例说明了,如果有其他线程使用锁对象,
偏向锁
会被撤销,升级为轻量级锁
;但是,如果多个线程之间没有竞争,那么,有可能不需要撤销偏向锁,而是进行
重偏向
(重置锁对象的thread) -
重偏向的条件:
偏向锁撤销的次数达到阈值(默认为20),并且没有锁竞争,就会将
撤销偏向锁
的动作改为重偏向
。 -
重偏向的阈值:
默认为20,可以通过JVM参数
-XX:BiasedLockingBulkRebiasThreshold=20
设置。
【举例说明】
/**
* VM options:-XX:BiasedLockingStartupDelay=0
*/
@Slf4j
public class RebiasTest {
public static void main(String[] args) {
Vector<Object> lockVector = new Vector<>();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
Object lock = new Object();
lockVector.add(lock);
synchronized (lock) {
System.out.println("[t1] - [" + i + "]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
synchronized (lockVector) {
lockVector.notify();
}
}, "t1").start();
new Thread(() -> {
synchronized (lockVector) {
try {
lockVector.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
for (int i = 0; i < 30; i++) {
Object lock = lockVector.get(i);
System.out.println("[t2] - [" + i + "] - [before]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
System.out.println("[t2] - [" + i + "] - [in]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
System.out.println("[t2] - [" + i + "] - [after]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}, "t2").start();
}
}
这里截取部分打印结果看一下
[t1] - [0]
0 8 (object header: mark) 0x0000000126857805 (biased: 0x000000000049a15e; epoch: 0; age: 0)
[t1] - [1]
0 8 (object header: mark) 0x0000000126857805 (biased: 0x000000000049a15e; epoch: 0; age: 0)
// ......
[t1] - [29]
0 8 (object header: mark) 0x0000000126857805 (biased: 0x000000000049a15e; epoch: 0; age: 0)
// t2线程前19次,撤销偏向锁,升级为轻量级锁
[t2] - [0] - [before]
0 8 (object header: mark) 0x0000000126857805 (biased: 0x000000000049a15e; epoch: 0; age: 0)
[t2] - [0] - [in]
0 8 (object header: mark) 0x000000016e31a940 (thin lock: 0x000000016e31a940)
[t2] - [0] - [after]
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
// ......
[t2] - [18] - [before]
0 8 (object header: mark) 0x0000000126857805 (biased: 0x000000000049a15e; epoch: 0; age: 0)
[t2] - [18] - [in]
0 8 (object header: mark) 0x000000016e31a940 (thin lock: 0x000000016e31a940)
[t2] - [18] - [after]
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
// t2线程从20次开始,不再撤销偏向锁,而是重偏向-重置了thread(可以看到前面的位数发生了变化)
[t2] - [19] - [before]
0 8 (object header: mark) 0x0000000126857805 (biased: 0x000000000049a15e; epoch: 0; age: 0)
[t2] - [19] - [in] VALUE
0 8 (object header: mark) 0x000000012685a905 (biased: 0x000000000049a16a; epoch: 0; age: 0)
[t2] - [19] - [after]
0 8 (object header: mark) 0x000000012685a905 (biased: 0x000000000049a16a; epoch: 0; age: 0)
// ......
[t2] - [29] - [before]
0 8 (object header: mark) 0x0000000126857805 (biased: 0x000000000049a15e; epoch: 0; age: 0)
[t2] - [29] - [in] VALUE
0 8 (object header: mark) 0x000000012685a905 (biased: 0x000000000049a16a; epoch: 0; age: 0)
[t2] - [29] - [after]
0 8 (object header: mark) 0x000000012685a905 (biased: 0x000000000049a16a; epoch: 0; age: 0)
9、批量撤销
-
批量撤销的概念:
偏向锁撤销的次数达到阈值(默认为20),并且没有锁竞争,就会将
撤销偏向锁
的动作改为重偏向
。但是如果重偏向之后的锁对象又被第三个线程使用,那么此时不是直接重偏向到第三个对象,而是
撤销偏向锁
也就是说,偏向锁撤销的次数会超过20次
而批量撤销就是,偏向锁撤销的次数会超过阈值(默认为40),这个类的所有对象都会变为不可偏向的(包括新建的)
-
批量撤销的阈值:
默认为40,可以通过JVM参数
-XX:BiasedLockingBulkRevokeThreshold=40
设置
【举例说明】
按照线程 t1、t2、t3的顺序执行,t1线程前19个会发生撤销;
/**
* 偏向锁_批量撤销
* VM options:-XX:BiasedLockingStartupDelay=0
*/
public class BatchRevokeBiasedLockTest {
static Thread t1, t2, t3;
public static void main(String[] args) throws InterruptedException {
Vector<Object> lockVector = new Vector<>();
int loopNumber = 39;
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Object lock = new Object();
lockVector.add(lock);
synchronized (lock) {
System.out.println("[t1] - [" + i + "]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
t2 = new Thread(() -> {
LockSupport.park();
for (int i = 0; i < loopNumber; i++) {
Object lock = lockVector.get(i);
System.out.println("[t2] - [" + i + "] - [before]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
System.out.println("[t2] - [" + i + "] - [in]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
System.out.println("[t2] - [" + i + "] - [after]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
t3 = new Thread(() -> {
LockSupport.park();
for (int i = 0; i < loopNumber; i++) {
Object lock = lockVector.get(i);
System.out.println("[t3] - [" + i + "] - [before]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
System.out.println("[t3] - [" + i + "] - [in]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
System.out.println("[t3] - [" + i + "] - [after]");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}, "t3");
t3.start();
t3.join();
System.out.println("批量撤销后,这个类的所有对象都会变为不可偏向的");
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
}
}
// 前面的打印信息这里就不展示了
....
批量撤销后,这个类的所有对象都会变为不可偏向的
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) // 0 01 无锁
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
七、小结
偏向锁
、轻量级锁
和重量级锁
是 Java 中用于实现线程同步的三种不同级别的锁。
- 偏向锁(Biased Locking):
- 当一个线程访问同步代码块并获取锁时,JVM 会假设这个线程会继续访问该代码块,因此会将锁标记为偏向该线程。
- 如果其他线程试图访问同步块,偏向锁会被撤销,线程重新竞争锁。
- 目标是在无竞争情况下,尽量减少同步的开销。
- 适用于只有一个线程访问同步块的场景。
- 轻量级锁(Lightweight Locking):
- 当多个线程使用同一个锁,但是不存在竞争时,JVM 会将锁标记为轻量级锁。
- 使用 CAS 操作来尝试获取锁。如果获取成功,线程可以进入临界区执行;否则,进入自旋等待状态。
- 自旋等待的目标是等待其他线程释放锁,避免线程切换的开销。
- 适用于多个线程访问同步代码块但是没有锁竞争的情况。
- 重量级锁(Heavyweight Locking):
- 当自旋等待无效或超过一定次数导致CAS失败时,JVM 会将轻量级锁膨胀为重量级锁。
- 重量级锁会将竞争失败的线程阻塞,降低 CPU 的占用率。
- 线程被阻塞时会进入阻塞队列,等待其他线程释放锁。
- 适用于长时间或高并发情况下的同步场景。
这些锁的选择取决于同步代码块的竞争程度、持有锁的时间以及系统的并发性能需求。