Monitor原理
Monitor 被翻译为监视器或管程
每个java对象都可以关联一个Monitor对象,如果使用synchronized给对像上锁(到重量锁级别)之后,该对象的MarkWork中的值被设置指向Monitor对象的指针。
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 BLOCKED - Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,这是线程的竞争是非公平的
-
图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足(调用了wait())进入 WAITING 状态的线程,这是需要notify/notifyAll唤醒。
注意点:
-
上述效果是synchronized必须是同一个对象,monitor才有上述的效果
-
没有通过synchronized的对象是不会关联monitor的
synchronized原理
如下代码
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
对应的字节码
public static void main(java.lang.String[]);
descriptor:([Ljava/lang/String;)V
flags:ACC_PUBLIC,ACC_STATIC
Code:
stack=2,locals=3,args_size=1
0:getstatic #2 // <- lock引用 (synchronized开始)
3:dup
4:astore_1 // lock引用 -> slot 1
5:monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6:getstatic #3 // <- i
9:iconst_1 // 准备常数 1
10:iadd // +1
11:putstatic #3 // -> i
14:aload_1 // <- lock引用
15:monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16:goto 24
19:astore_2 // e -> slot 2
20:aload_1 // <- lock引用
21:monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22:aload_2 // <- slot 2 (e)
23:athrow // throw e
24:return
Exception table:
from to
target type
6 16 19any
19 22 19any
LineNumberTable:
line 8:0
line 9:6
line 10:14
line 11:24
LocalVariableTable:
Start Length
Slot Name
Signature
0 25 0 args [Ljava/lang/String;
StackMapTable:number_of_entries =2
frame_type =255 /* full_frame */
offset_delta =19
locals =[class "[Ljava/lang/String;",
class java/lang/Object ]
stack =[
class java/lang/Throwable ]
frame_type =250 /* chop */
offset_delta =4
在该字节码中
5:monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6:getstatic #3 // <- i
9:iconst_1 // 准备常数 1
10:iadd // +1
11:putstatic #3 // -> i
14:aload_1 // <- lock引用
15:monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
这便是java代码块中的同步代码块的部分,也就是被synchronized修饰的部分代码块。
这也说明synchronized底层就是使用monitor对象关联来实现的。
synchronized 原理进阶
在jdk1.6之后,synchronized锁被优化,有一个锁升级的过程。
synchronized锁对象升级
过程分四个阶段:
无锁–>偏向锁–>轻量级锁–>重量级锁
。
无锁:
新创建的对象,第一次被sychronized修饰为锁对象,当时该对象的后三位仍然是
001
,这是因为sychronized对偏向锁设置有个延时过程。需要过几秒才会变为偏向状态
101
可以通过
XX:BiasedLockingStartupDelay=0
来禁用延迟。
偏向锁:
jvm记录只有第一次使用的cas来将线程ID设置到对象的MarkWord头,之后再次需要获得该对象锁时发现这个线程ID是自己的就表示没有竞争,不用重新cas。只要不要说竞争,之后这个对象锁就一直属于该线程,降低了开销。偏向锁后三位为
101
轻量级锁:
synchronized优化经过偏向锁阶段后,这时又来新线程进行竞争该锁对象,新的线程是通过cas去进行自旋竞争该对象锁。若是cas自旋获得该对象锁,则当前对象锁升级为轻量级锁,轻量级的对象锁对象头设置拥有其的线程ID,后两位为
00
。
重量级锁:
锁膨胀
(锁粗化)的过程,当cas自旋获取对象锁失败,如果是其他线程已经持有该对象锁,这时表明有竞争,当前对象锁升级为对象锁。这是就需要引用monitor对象,锁对象的对象头设置其对应的monitor对象指针,后两位为
11
四种锁对象头对应表
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID。
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。
批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
锁消除
锁消除,时jvm中的JIT(即时编译器)可以识别到,当前锁对象一定不会被竞争重用时,就会将锁的逻辑不在底层编译出来。
如下,局部变量o时不能被重用的,因为没错调用b()方法,都会创建新的o对象,所以这时就会有锁消除。
public class MyBenchmark {
static int x = 0;
public void a() throws Exception {
x++;
}
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
}