Concurrency Programming 三
synchronized的3种锁
- 偏向锁 2. 轻量级锁 3. 重量级锁
1. 重量级锁: Monitor
- 通过 synchronized给对象上"重量级锁"时, 会对该对象头的 Mark Word内设置指向 Monitor对象的指针
- Mark Word基本结构:
Mark Word (32 bits) | 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_monitor:30 | 10 | Heavyweight Locked(重量级锁) |
| 11 | Marked for GC |
Mark Word (64 bits) | State |
---|---|
unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased(偏向锁) |
ptr_to_lock_record:62 | 00 | Lightweight Locked(轻量级锁) |
ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked(重量级锁) |
| 11 | Marked for GC |
- Monitor结构:
- 起初 Monitor的 Owner为 null
- 当 Thread-2执行 synchronized(obj)就会将 Monitor的 "Owner"设置为 Thread-2, 同一个时刻 Monitor只能有一个 “Owner”
- 在 Thread-2上锁的过程中, 如果 Thread-3, Thread-4, Thread-5线程, 执行 synchronized(obj), 则会进入 “EntryList”, 同时这3个线程状态, 被更改为 “BLOCKED”
- Thread-2执行完同步代码块后, 唤醒 "EntryList"内等待的线程(如有多个阻塞中的线程, 则会非公平方式来竞争锁)
- 图中 "WaitSet"内的 Thread-0和 Thread-1是之前获得过锁, 但条件不满足进入 "WAITING"状态的线程, 后面讲"wait& notify"时会分析
*注: 1. synchronized必须进入同一个对象的 Monitor才会有上述效果 2. 未加 synchronized的对象是不会关联 Monitor
2. 轻量级锁
*特点: 轻量级锁没有阻塞, 如果当前锁主为相同线程, 则所重入. 否则进入锁膨胀(轻量级锁变为重量级锁)
- 使用场景: 如果一个对象虽然有多线程要加锁, 但加锁的时间是错开的(也就是没有竞争), 则可以使用轻量级锁来优化
- 加锁过程:
- 两个同步块, 利用同一个对象加锁
final static Object obj = new Object();
public static void main(String[] args) {
method1();
}
public static void method1() {
synchronized(obj) { // 同步块 A
method2();
}
}
public static void method2() {
synchronized(obj) { // 同步块 B
}
}
- 线程创建 Lock Record(每个线程的栈帧都包含一个锁记录结构), 内部可以存储锁对象的 Mark Word
- Lock Record的 Object reference指向锁对象, 尝试用 CAS(Compare and Swap, 比较和替换)比较替换锁对象的 Mark Word, 并将 Mark Word的值存入 "Lock Record地址00"中
- 如果 CAS成功, 则对象头中也存入了 “Lock Record地址00”, 表示由该线程加了轻量级锁
- 如果 CAS失败, 则有两种情况
5.1 如果其它线程已经持有了该锁对象的轻量级锁, 则表明有竞争, 进入"锁膨胀"过程
5.2 如果之前的同步块代码未执行完, 该相同线程, 再次执行 synchronized, 则锁重入, 此时该线程内再添加一条 Lock Record作为重入的计数
- 同步代码块执行完后(解锁):
6.1 当解锁时, 如果有取值为 null的锁记录, 表示有重入, 此时重置锁记录, 重入计数减一
6.2 当解锁时, 锁记录的值不为 null, 此时使用 CAS, 将 Mark Word的值恢复到锁对象
6.2.1 CAS成功, 则解锁成功
6.2.2 CAS失败, 则轻量级锁进行了"锁膨胀"或已升级为"重量级锁", 进入重量级锁的解锁流程
2.1 锁膨胀(轻量级锁变为重量级锁)
- 当尝试加轻量级锁的过程中, CAS无法成功, 这表明其它线程已经持有了该锁对象的锁(有竞争), 此时需要进行锁膨胀. 会将当前轻量级锁变为重量级锁, 进入锁阻塞等待状态
- 锁膨胀过程:
- 当 Thread-1进行轻量级加锁时, CAS失败. 因为 Thread-0已经对该锁对象加了锁
- Thread-1加轻量级锁失败后:
2.1 为锁对象申请 Monitor锁(重量级锁). 让锁对象的 Mark Word指向 Monitor
2.2 将当前线程加入到 Monitor.EntryList, 同时, 把当前线程状态更改为 “BLOCKED”
- 当 Thread-0退出同步块解锁时, 使用 CAS, 将 Mark Word的值恢复到锁对象. 如果失败, 则会进入重量级解锁流程, 即按照锁对象内的 Monitor地址找到 Monitor对象, 设置 Owner为 null, 再唤醒 EntryList中 BLOCKED状态的线程(让阻塞等待的线程非公平方式来竞争锁)
2.2 自旋优化(Adaptive Spinning)
- 重量级锁竞争的时候, 还可以使用自旋来进行优化, 也就是"竞争线程"不会直接进入 Monitor的 “EntryList”(阻塞等待), 而是会自旋重试
- 如果, 此时, 持锁线程退出了同步块, 并释放了锁, 则可以避免线程上下文切换
- 或自旋重试几下(次数为自适应的)后, 依然未能拿到锁, 则进入 “EntryList”(阻塞等待)
* 适合在多核CPU, 在单核 CPU上是浪费
* Java 6之后自旋重试是自适应的, 比如上一次自旋操作成功过, 那么认为这次自旋成功的可能性会高, 就会多自旋几次. 反之, 就少自旋, 甚至不自旋. Java 7之后不能控制是否开启自旋功能
3. 偏向锁(Biased Locking)
-
轻量级锁在没有竞争时(相同线程之间), 每次重入, 仍需要做 CAS.
Java 6开始引入了偏向锁来做进一步优化. 第一次使用 CAS, 将线程 ID("Lock Record地址00"), 设置到锁对象的 Mark Word后, 第二次执行发现持锁线程与当前线程相同, 也就是没有竞争, 则不再重新 CAS. 只要以后不再发生竞争, 该锁对象就归该线程所有
-
一个对象创建时:
- 如果开启了偏向锁(默认开启), 对象头上的 Mark Word值的后3位为101, 以及 thread, epoch, age都为0
- 如果没有开启偏向锁, 则对象头上的 Mark Word值的后3位为001, 以及 hashcode, age都为0, 正常状态对象一开始是没有 hashCode, 第一次调用后才会生成
-处于偏向锁的对象解锁后, 线程 ID, 仍保留在对象头中
* 注: 偏向锁默认是延迟的, 不会在程序启动时立即生效. 可以设置立即生效加 VM参数 -XX:BiasedLockingStartupDelay=0不延迟
* 禁用偏向锁参数 -XX:-UseBiasedLocking
3.1 撤销
-
调用锁对象的 hashCode方法, 会导致撤销偏向锁
1.1 轻量级锁: 会在锁记录中记录 hashCode
1.2 重量级锁: 会在 Monitor中记录 hashCode -
当有其它线程使用偏向锁对象时, "可偏向"会被撤销升级为轻量级锁
3.2 批量重偏向
- 当多个锁对象偏向了 T1线程时, 可以将多个锁对象批量重新偏向 T2线程(重偏向时, 会重置锁对象的 Mark Word上的线程 ID)
- 偏向 T1线程的每个锁对象换 T2线程时, 前19个锁对象都会, 撤销偏向锁(升级为轻量级锁), 然后解锁后, 将锁对象头的 Mark Word值的后3位改为001(Normal)
- 直到阈值(20), 也就是第20个锁对象开始会批量自动重偏向为 T2线程
3.3 批量撤销
- 当撤销偏向锁阈值到了40次后(3个或以上线程间竞争), JVM认为竞争太激烈了, 于是不在加偏向锁了, 同时将整个类的所有对象都会变为不可偏向的, 包括新建的对象
3.4 锁消除优化(Lock Elimination)
- 加锁和不加锁性能比较
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
@Fork(1)
@BenchmarkMode(Mode.AverageTime) // 计算平均运行时间
@Warmup(iterations=3) // 预热次数, 代码进行优化
@Measurement(iterations=5) // 测试次数
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 运行时间单位是纳秒
public class MyBenchmark {
static int x = 0;
@Benchmark
public void a() throws Exception {
x++;
}
@Benchmark
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
}
> java -jar benchmarks.jar
Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op
c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op
# 两个方法分数差不多的原因是 Java的 JIT即时编译器(默认开启"锁销除" -XX:+EliminateLocks), 自动优化掉了代码. 比如 b()方法内的 Object o = new Object(), 由于它是局部变量, 根本不会被共享, 所以 synchronized同步块没有意义, 因此被优化掉了
> java -XX:-EliminateLocks -jar benchmarks.jar
Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.507 0.108 ns/op
c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op
# 关闭锁消除优化 -XX:-EliminateLocks后, 发现分数差距很大
3.5 锁粗化优化(Lock Coarsening)
- 一般建议, 将同步块的代码作用范围尽量小. 不过一系列的连续操作都对同一个对象反复加锁和解锁, 甚至加锁操作出现在循环体中, 那即使没有线程竞争, 也会导致不必要的性能损耗. 此时 JVM会做"锁粗化"(同步代码块的范围放大)优化
如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!