并发编程(三)

92 篇文章 0 订阅
10 篇文章 0 订阅

synchronized的3种锁

  1. 偏向锁 2. 轻量级锁 3. 重量级锁

1. 重量级锁: Monitor

  • 通过 synchronized给对象上"重量级锁"时, 会对该对象头的 Mark Word内设置指向 Monitor对象的指针
  • Mark Word基本结构:
Mark Word (32 bits)State
hashcode:25 | age:4 | biased_lock:0 | 01Normal
thread:23 | epoch:2 | age:4 | biased_lock:1 | 01Biased(偏向锁)
ptr_to_lock_record:30 | 00Lightweight Locked(轻量级锁)
ptr_to_heavyweight_monitor:30 | 10Heavyweight Locked(重量级锁)
| 11Marked for GC
Mark Word (64 bits)State
unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01Normal
thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01Biased(偏向锁)
ptr_to_lock_record:62 | 00Lightweight Locked(轻量级锁)
ptr_to_heavyweight_monitor:62 | 10Heavyweight Locked(重量级锁)
| 11Marked for GC
  • Monitor结构:

在这里插入图片描述

  1. 起初 Monitor的 Owner为 null
  2. 当 Thread-2执行 synchronized(obj)就会将 Monitor的 "Owner"设置为 Thread-2, 同一个时刻 Monitor只能有一个 “Owner”
  3. 在 Thread-2上锁的过程中, 如果 Thread-3, Thread-4, Thread-5线程, 执行 synchronized(obj), 则会进入 “EntryList”, 同时这3个线程状态, 被更改为 “BLOCKED”
  4. Thread-2执行完同步代码块后, 唤醒 "EntryList"内等待的线程(如有多个阻塞中的线程, 则会非公平方式来竞争锁)
  5. 图中 "WaitSet"内的 Thread-0和 Thread-1是之前获得过锁, 但条件不满足进入 "WAITING"状态的线程, 后面讲"wait& notify"时会分析
    *注: 1. synchronized必须进入同一个对象的 Monitor才会有上述效果 2. 未加 synchronized的对象是不会关联 Monitor

2. 轻量级锁

  • *特点: 轻量级锁没有阻塞, 如果当前锁主为相同线程, 则所重入. 否则进入锁膨胀(轻量级锁变为重量级锁)
  • 使用场景: 如果一个对象虽然有多线程要加锁, 但加锁的时间是错开的(也就是没有竞争), 则可以使用轻量级锁来优化
  • 加锁过程:
  1. 两个同步块, 利用同一个对象加锁

    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
        }
    }

  1. 线程创建 Lock Record(每个线程的栈帧都包含一个锁记录结构), 内部可以存储锁对象的 Mark Word
  2. Lock Record的 Object reference指向锁对象, 尝试用 CAS(Compare and Swap, 比较和替换)比较替换锁对象的 Mark Word, 并将 Mark Word的值存入 "Lock Record地址00"中

在这里插入图片描述

  1. 如果 CAS成功, 则对象头中也存入了 “Lock Record地址00”, 表示由该线程加了轻量级锁

在这里插入图片描述

  1. 如果 CAS失败, 则有两种情况
    5.1 如果其它线程已经持有了该锁对象的轻量级锁, 则表明有竞争, 进入"锁膨胀"过程
    5.2 如果之前的同步块代码未执行完, 该相同线程, 再次执行 synchronized, 则锁重入, 此时该线程内再添加一条 Lock Record作为重入的计数

在这里插入图片描述

  1. 同步代码块执行完后(解锁):
    6.1 当解锁时, 如果有取值为 null的锁记录, 表示有重入, 此时重置锁记录, 重入计数减一
    6.2 当解锁时, 锁记录的值不为 null, 此时使用 CAS, 将 Mark Word的值恢复到锁对象
    6.2.1 CAS成功, 则解锁成功
    6.2.2 CAS失败, 则轻量级锁进行了"锁膨胀"或已升级为"重量级锁", 进入重量级锁的解锁流程

2.1 锁膨胀(轻量级锁变为重量级锁)

  • 当尝试加轻量级锁的过程中, CAS无法成功, 这表明其它线程已经持有了该锁对象的锁(有竞争), 此时需要进行锁膨胀. 会将当前轻量级锁变为重量级锁, 进入锁阻塞等待状态
  • 锁膨胀过程:
  1. 当 Thread-1进行轻量级加锁时, CAS失败. 因为 Thread-0已经对该锁对象加了锁

在这里插入图片描述

  1. Thread-1加轻量级锁失败后:
    2.1 为锁对象申请 Monitor锁(重量级锁). 让锁对象的 Mark Word指向 Monitor
    2.2 将当前线程加入到 Monitor.EntryList, 同时, 把当前线程状态更改为 “BLOCKED”

在这里插入图片描述

  1. 当 Thread-0退出同步块解锁时, 使用 CAS, 将 Mark Word的值恢复到锁对象. 如果失败, 则会进入重量级解锁流程, 即按照锁对象内的 Monitor地址找到 Monitor对象, 设置 Owner为 null, 再唤醒 EntryList中 BLOCKED状态的线程(让阻塞等待的线程非公平方式来竞争锁)

2.2 自旋优化(Adaptive Spinning)

  • 重量级锁竞争的时候, 还可以使用自旋来进行优化, 也就是"竞争线程"不会直接进入 Monitor的 “EntryList”(阻塞等待), 而是会自旋重试
  1. 如果, 此时, 持锁线程退出了同步块, 并释放了锁, 则可以避免线程上下文切换
  2. 或自旋重试几下(次数为自适应的)后, 依然未能拿到锁, 则进入 “EntryList”(阻塞等待)
    * 适合在多核CPU, 在单核 CPU上是浪费
    * Java 6之后自旋重试是自适应的, 比如上一次自旋操作成功过, 那么认为这次自旋成功的可能性会高, 就会多自旋几次. 反之, 就少自旋, 甚至不自旋. Java 7之后不能控制是否开启自旋功能

3. 偏向锁(Biased Locking)

  • 轻量级锁在没有竞争时(相同线程之间), 每次重入, 仍需要做 CAS. Java 6开始引入了偏向锁来做进一步优化. 第一次使用 CAS, 将线程 ID("Lock Record地址00"), 设置到锁对象的 Mark Word后, 第二次执行发现持锁线程与当前线程相同, 也就是没有竞争, 则不再重新 CAS. 只要以后不再发生竞争, 该锁对象就归该线程所有

  • 一个对象创建时:

  1. 如果开启了偏向锁(默认开启), 对象头上的 Mark Word值的后3位为101, 以及 thread, epoch, age都为0
  2. 如果没有开启偏向锁, 则对象头上的 Mark Word值的后3位为001, 以及 hashcode, age都为0, 正常状态对象一开始是没有 hashCode, 第一次调用后才会生成
    -处于偏向锁的对象解锁后, 线程 ID, 仍保留在对象头中
    * 注: 偏向锁默认是延迟的, 不会在程序启动时立即生效. 可以设置立即生效加 VM参数 -XX:BiasedLockingStartupDelay=0不延迟
    * 禁用偏向锁参数 -XX:-UseBiasedLocking

3.1 撤销

  1. 调用锁对象的 hashCode方法, 会导致撤销偏向锁
    1.1 轻量级锁: 会在锁记录中记录 hashCode
    1.2 重量级锁: 会在 Monitor中记录 hashCode

  2. 当有其它线程使用偏向锁对象时, "可偏向"会被撤销升级为轻量级锁

3.2 批量重偏向

  1. 当多个锁对象偏向了 T1线程时, 可以将多个锁对象批量重新偏向 T2线程(重偏向时, 会重置锁对象的 Mark Word上的线程 ID)
  2. 偏向 T1线程的每个锁对象换 T2线程时, 前19个锁对象都会, 撤销偏向锁(升级为轻量级锁), 然后解锁后, 将锁对象头的 Mark Word值的后3位改为001(Normal)
  3. 直到阈值(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会做"锁粗化"(同步代码块的范围放大)优化

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值