synchronized底层加锁原理
synchronized加锁主要是通过对象头和监视器实现的,下面我们具体介绍
1. 对象头
Mark Word:这是对象头的主要部分,用于存储对象的运行时元数据,如对象的哈希码、对象的分代年龄、锁状态标志、偏向锁 ID 等信息。它的长度一般为 32 位或 64 位,具体取决于 Java 虚拟机的运行模式和操作系统的位数。在不同的锁状态下,Mark Word 会存储不同的信息,以支持高效的锁机制。
Class Pointer:即对象指向它的类元数据的指针,通过这个指针,虚拟机可以知道该对象属于哪个类,从而能够在运行时获取对象的类信息,如类的方法、字段等。在 64 位虚拟机中,默认情况下,这个指针占用 64 位空间。不过,为了节省内存,虚拟机可能会开启指针压缩功能,此时 Class Pointer 的长度可能会变为 32 位。
- 偏向锁ID:由一个bit存储,1表示偏向锁,0表示非偏向锁
- 锁状态标志:由两个bit存储,01表示无锁,00表示轻量级锁,10表示重量级锁,11表示GC标记
线程会根据这两个标记判断这个锁对象是否上锁。
2. 监视器(Monitor)
监视器可以理解成一种同步工具,也可以看作是一种锁的实现。在 Java 里,每个 Java 对象都可以关联一个监视器。当一个线程尝试访问被监视器保护的代码块或方法时,它必须先获取该对象的监视器锁。若该监视器已被其他线程持有,那么这个线程就会进入阻塞状态,直至锁被释放。
监视器主要由三部分构成:
- Owner:此为持有监视器锁的线程。同一时间,仅有一个线程能够成为监视器的 Owner。
- Entry Set:也就是入口集,这里存放着所有尝试获取监视器锁但当前处于阻塞状态的线程。当 Owner 线程释放锁之后,Entry Set 中的线程会竞争获取锁。
- Wait Set:即等待集,这里存放着那些调用了对象的wait()方法之后进入等待状态的线程。当其他线程调用了相同对象的notify()或者notifyAll()方法时,Wait Set 中的线程会被唤醒,之后它们会和 Entry Set 中的线程一起竞争获取锁。
监视器的工作原理
- 获取锁:当线程执行到synchronized修饰的方法或者代码块时,线程会尝试获取该对象的监视器锁。若锁处于空闲状态(查看对象头的锁标志,偏向锁id),线程就能获取到锁,然后成为监视器的 Owner,进而执行同步代码。
- 释放锁:当线程执行完同步代码后,会释放持有的监视器锁。若此时 Entry Set 中有等待的线程,这些线程会竞争获取锁。
- 等待和唤醒:当线程在同步代码中调用对象的wait()方法时,线程会释放持有的监视器锁,然后进入该对象的 Wait Set。当其他线程调用相同对象的notify()或者notifyAll()方法时,Wait Set 中的线程会被唤醒,之后它们会和 Entry Set 中的线程一起竞争获取锁。
3. 细节补充
3.1 获取锁
当线程执行到 synchronized 修饰的方法或者代码块时,线程会尝试获取该对象的监视器锁。这一过程在不同锁状态下具体细节有所不同:
- 无锁状态:无锁状态下,对象头的 Mark Word 存储对象的哈希码、分代年龄等信息。当第一个线程访问同步块并尝试获取锁时,会检查对象头的锁标志位。如果是无锁状态,会通过 CAS(Compare - And - Swap)操作将线程 ID 记录到对象头的 Mark Word 中,同时将锁标志位设置为偏向锁,此时该线程成为监视器的 Owner。
- 偏向锁状态:会检查 Mark Word 中记录的偏向线程 ID 是否是当前线程。如果是,则直接获取锁;如果不是,则需要撤销偏向锁,将对象头恢复到无锁状态或者升级为轻量级锁,再重新竞争锁。
- 轻量级锁状态:线程在进入同步块时,会在当前线程的栈帧中创建一个锁记录(Lock Record),然后通过 CAS 操作尝试将对象头的 Mark Word 复制到锁记录中,并将对象头的 Mark Word 指向锁记录的地址。如果 CAS 操作成功,线程获取到轻量级锁,成为监视器的 Owner;如果失败,表示有其他线程已经持有轻量级锁,当前线程会进行自旋等待,若自旋一定次数后仍未获取到锁,轻量级锁会升级为重量级锁。
- 重量级锁状态:重量级锁依赖于操作系统的互斥量(Mutex)实现。当线程尝试获取重量级锁时,如果锁处于空闲状态,线程可以直接获取;如果锁已被其他线程持有,线程会被阻塞,进入 Entry Set 等待。
3.2 释放锁
当线程执行完同步代码后,会释放持有的监视器锁。释放锁的过程同样与锁状态有关:
- 偏向锁释放:偏向锁在没有竞争的情况下不会主动释放,只有当有其他线程尝试竞争锁时,持有偏向锁的线程才会在安全点(Safe Point)上撤销偏向锁。
- 轻量级锁释放:线程退出同步块时,会使用 CAS 操作将锁记录中的 Mark Word 复制回对象头。如果 CAS 操作成功,说明没有发生锁竞争,轻量级锁释放成功;如果失败,说明在释放锁的过程中发生了锁竞争,轻量级锁已经升级为重量级锁,需要按照重量级锁的方式释放。
- 重量级锁释放:持有重量级锁的线程执行完同步代码后,会释放操作系统的互斥量,唤醒 Entry Set 中等待的线程,这些线程会竞争获取锁。
3.3 等待和唤醒
- notify() 方法:会随机唤醒 Wait Set 中的一个线程,被唤醒的线程需要重新竞争锁,只有竞争到锁才能继续执行。
- notifyAll() 方法:会唤醒 Wait Set 中的所有线程,这些线程会和 Entry Set 中的线程一起竞争锁。