一、Monitor
1.定义
管程,为了解决操作系统级别关于线程同步原语的使用复杂性,对复杂操作进行封装。
2.作用
限制同一时刻,只有一个线程能进入monitor框定的临界区,达到线程互斥,保护临界区中临界资源的安全。
作为同步工具,它也提供了管理进程,线程状态的机制,比如monitor能管理因为线程竞争未能第一时间进入临界区的其他线程,并提供适时唤醒的功能。
3.组成
- monitor对象
- 临界区
- 条件变量
- 定义在monitor对象上的wait() signal() signalAll()操作
二、Java对象的组成
- 对象头
- 实例数据:对象的实例数据就是在java代码中能看到的属性和他们的值。
- 对齐填充:因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。
1.对象头
组成:
- MarkWord(32 bits)
- Klass Word (32 bits):指向类的指针(找到类对象,存储自己是什么类型),Java对象的类数据保存在方法区。
- 数组长度(只有数组对象才有)(32 bits)
MarkWord
Mark Word记录了对象和锁有关的信息。在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中:


当线程进入临界区的时候,锁住的对象中,markword记录了指向monitor的指针,并把锁标志位从01改成10,前面的对象hashcode等信息也丢弃,存储指向monitor对象的指针(占用30位)。monitor中的owner存储了当前线程的信息。
- 刚开始 Monitor 中 Owner 为 null
- 当Thread-1 执行 synchronized(obi) 就会将 Monitor 的所有者 Owner 置为Thread-1,Monitor中只能有一个Owner
- 在 Thread-1 上锁的过程中,如果 Thread-2,Thread-3也来执行 synchronized(obi),就会进入EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
- WaitSet 中的 Thread-4是之前获得过锁,但条件不满足,进入 WAITING 状态的线程。
当第二个线程来竞争时,会检查锁对象是否关联了monitor,关联了再检查monitor的owner是否有值,如果有值,就加入entryList的等待队列中,进入block阻塞状态。线程1如果执行完代码了,owner值为空,会唤醒entryList等待队列中的线程,再竞争,成为下一个owner。
synchronized必须是进入同一个对象的monitor才有效果;不加synchronized的对象不会关联监视器,不遵从以上规则。
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args){
synchronized (lock){
counter++;
}
}
对应的字节码:

三、轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的 (也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁:
static final Object obj = new Object();
public static void method1() {
synchronized(obj) {
// 同步块 A
method2();
}
public static void method2() {
synchronized(obj) {
// 同步块 B
}
}
创建锁记录 (Lock Record) 对象,每个线程的栈都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word:
锁记录中Object reference 指向锁对象,并尝试cas替换Object的Mark Word,将markword的值存入锁记录。

如果 cas 失败,有两种情况
- 如果是其它线程已经持有了该 Obiect 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入,那么再添加一条Lock Record 作为重入的计数

当退出Synchronized代码块时(解锁时),如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减1。

当退出Synchronized代码块时(解锁时),锁记录的值不为null,这时使用CAS将markword的值恢复给对象头:
- 恢复成功:解锁成功
- 恢复失败:说明轻量级锁进入了锁膨胀或已经升级为重量级锁,进入重量级锁解锁过程
四、 轻量级锁优化:偏向锁
轻量级锁在没有竞争时 (就只有自己这一个线程),每次重入仍然需要执行 CAS 操作,Java 6 中引入了偏向锁来做进一步优化:
只有第一次使用 CAS 将线程ID设置到对象的 Mark Word 头(不再像轻量级锁一样存储锁记录的地址),之发现这个线程ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
static final Object obj = new object()
public static void m1() {
synchronized(obj) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized(obj) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized(obj) {
// 同步块 C
m3();
}
}


1.偏向状态
Mark Word(64bit) | 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 |
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后3 位为 101,这时它的thread、epoch、age 都为 0
- 偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 JVM 参数-XX:BiasedLockingStartupDelay= 0 来禁止延迟
- 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后3 位为 001,这时它的 hashcode、 age都为 0,第一次用到 hashcode 时才会赋值
- 处于偏向锁的对象解锁后,线程id仍存储于对象头中。
禁用偏向锁:-XX:-UseBiasedLocking
启用偏向锁:-XX:+UseBiasedLocking
2.偏向状态的撤销
1.调用对象的hashcode方法
当一个可偏向的对象调用了hashcode方法后,但偏向锁的对象 MarkWord 中存储的是线程 id,会撤销对象的偏向状态,变为不可偏向的正常对象。因为没有空间存储线程id了。
在被偏向锁住的临界代码中调用hashcode会怎么样?
- 轻量级锁的hashcode存储在栈帧的锁记录中
- 重量级锁的hashcode存储在monitor对象中
解锁会还原回来,所以不存在该问题。
2.其他线程使用对象
当有其他线程使用偏向锁对象时(两个线程没有竞争锁,否则就是升级为重量级锁),会将偏向锁升级为轻量级锁,偏向状态被撤销。
3.调用wait/notify
只有重量级锁有wait/notify方法。调用了之后就会把偏向锁/轻量级锁升级为重量级锁
3.批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID。
当撤销偏向锁阈值超过 20 次后,jvm 会这样认为偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程。
private static void test3() throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t2");
t2.start();
}
当t2去获取锁时,偏向锁会撤销,升级为轻量级锁,撤销次数达到阈值20次后,轻量级锁被批量重偏向为T2线程。即JVM会重新给当前t2线程批量设置偏向锁。
4.批量撤销
当撤销偏向锁阈值超过 40 次后,JVM 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
private static void test4() throws InterruptedException {
Vector<Dog> list = new Vector<>();
int loopNumber = 39;
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
t2 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
t3 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t3");
t3.start();
t3.join();
log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}
5.锁消除
JIT(即时编译器),对字节码进行优化。分析某些局部变量不会逃逸出方法,直接把synchronized去掉。
默认会开启,删除锁消除配置:-XX:-EliminateLocks
五、锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,一种情况是有其他线程在该对象上加了轻量级锁(有竞争),此时需要进行锁膨胀,将轻量级锁变为重量级锁。
当Thread1进行轻量级加锁时,Thread0已经对该对象加了轻量级锁:

Thread1加轻量级锁失败,进入锁膨胀流程:
- 为Obj对象申请Monitor锁,让Obj指向重量级锁地址
- Thread1进入Monitor的EntryList中Blocked

当Thread0退出同步块解锁时,使用CAS将markword的值恢复给对象头失败,进入重量级锁解锁流程,按照monitor地址找到monitor对象,设置owner为null,唤醒entryList中Blocked的线程
自旋优化
重量级锁竞争的时候,可以使用自旋进行优化,如果当前线程自旋成功(即持锁线程已经退出了同步块,释放了锁),当前线程就可以避免阻塞。
多核cpu下才有意义。
在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次,反之,就少自旋甚至不自旋。
自旋会占用 CPU 时间,单核 CPU 自旋没有意义,多核 CPU 自旋才能发挥优势
Java 7之后不能控制是否开启自旋功能
网友评论