1、Java中的对象头
普通对象
数组对象
Mark Word结构
Mark Word:
Java对象头中的Mark Word(标记字)是Java虚拟机为每个对象额外存储的一部分数据,用于辅助GC(垃圾回收)和线程同步等操作。Mark Word位于对象的内存布局最开始的位置。
Mark Word中常见的信息,对象的Hashcode、分代年龄、锁信息、对象标志位等。
Klass Word:
Klass Word可以被视为一个指针,它存储了关于类的元数据信息,包括类的类型、父类、方法表等。
2、Monitor(锁监视器、管程)
Monitor是由操作系统生成的对象,和Java中的对象一一对应,在JDK1.6锁优化之前,被synchronized修饰的对象,对象头的Mark Word会存放指向Monitor的指针,而Monitor会将Owner置为该线程。
在Thread-2未解锁之前,其他线程访问同步代码块,会进入EntryList阻塞队列中,当锁释放了才会唤醒。
3、字节码层面原理
代码:
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
字节码:
0: new #2 // new Object
3: dup
4: invokespecial #1 // invokespecial <init>:()V,非虚方法
7: astore_1 // lock引用 -> lock
8: aload_1 // lock (synchronized开始)
9: dup // 一份用来初始化,一份用来引用
10: astore_2 // lock引用 -> slot 2
11: monitorenter // 【将 lock对象 MarkWord 置为 Monitor 指针】
12: getstatic #3 // System.out
15: ldc #4 // "ok"
17: invokevirtual #5 // invokevirtual println:(Ljava/lang/String;)V
20: aload_2 // slot 2(lock引用)
21: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // slot 2(lock引用)
27: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
11-21是代码块加锁解锁过程
25-27是发生异常情况后也会释放锁
4、锁升级过程
Java中的锁升级可以分为
无锁 -》 偏向锁 -》 轻量级锁 -》重量级锁
4.1 偏向锁
当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作。
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低
- 当一个对象已经计算过 hashCode,就再也无法进入偏向状态了
- 添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
撤销偏向锁的状态:
- 调用对象的 hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode 导致偏向锁被撤销
- 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
- 调用 wait/notify,需要申请 Monitor,进入 WaitSet
4.2 轻量级锁
当多个线程竞争同一个锁时,JVM将锁从偏向锁态升级到轻量级锁态。在轻量级锁中,Mark Word中记录了指向锁记录(Lock Record)的指针。线程在获取轻量级锁时,会使用CAS(Compare and Swap)操作将Mark Word替换成指向自己的锁记录,如果CAS成功,则说明获取轻量级锁成功,可以进入同步块进行操作。如果CAS失败,则表示锁竞争失败,需要升级为重量级锁。
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word
让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
- 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁
- 如果 CAS 失败,有两种情况:
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数
- 当退出 synchronized 代码块(解锁时)
- 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
- 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
4.3 重量级锁
Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED
当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
5、锁优化
5.1 自旋锁
在重量级锁竞争的时候,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁
注意:
- 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
- 自旋失败的线程会进入阻塞状态
优点:不会进入阻塞状态,减少线程上下文切换的消耗
缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源
- 自旋成功
- 自旋失败
自适应自旋锁:
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
- Java 7 之后不能控制是否开启自旋功能,由 JVM 控制
//手写自旋锁
public class SpinLock {
// 泛型装的是Thread,原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " come in");
//开始自旋,期望值为null,更新值是当前线程
while (!atomicReference.compareAndSet(null, thread)) {
Thread.sleep(1000);
System.out.println(thread.getName() + " 正在自旋");
}
System.out.println(thread.getName() + " 自旋成功");
}
public void unlock() {
Thread thread = Thread.currentThread();
//线程使用完锁把引用变为null
atomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + " invoke unlock");
}
public static void main(String[] args) throws InterruptedException {
SpinLock lock = new SpinLock();
new Thread(() -> {
//占有锁
lock.lock();
Thread.sleep(10000);
//释放锁
lock.unlock();
},"t1").start();
// 让main线程暂停1秒,使得t1线程,先执行
Thread.sleep(1000);
new Thread(() -> {
lock.lock();
lock.unlock();
},"t2").start();
}
}
5.2 锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)
5.3 锁粗化
对相同的对象多次加锁,频繁的加锁操作会导致性能消耗,可以通过锁粗化的方式将加锁的返回扩大,减少加锁次数。
5.4 多把锁
将锁的粒度细分,增加并发度。
- 好处:增加并发度
- 坏处:可能出现死锁