1.Monitor 概念
1.1Java 对象头
32位虚拟机中,普通对象的对象头结构如下,其中的Klass Word为指针,指向对应的Class对象;
数组对象:
mark word结构:
所以一个对象的结构如下:
1.2.Monitor 原理
Monitor被翻译为监视器或者说管程
Java虚拟机(HotSpot)中,Monitor是通过ObjectMonitor实现的(c++),里面有三个重要的属性:
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
最重要的三个成员变量:
EntryList:线程获取monitor对象失败后会进入entrySet
WaitSet:线程调用wait()进入WaitSet
Owner:拥有锁的线程。
每个java对象都可以关联一个Monitor,如果使用synchronized
给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象的指针。
monitor工作原理:
当线程执行到了临界区代码的时候:
如果使用了syn锁,先会查询syn锁的对象是否绑定了monitor;
如果没有绑定,则会先去去与Monitor绑定,并且将Owner设为当前线程。
如果已经绑定,则会去查询该Monitor是否已经有了Owner
如果没有,则Owner与将当前线程绑定
如果有,则放入EntryList,进入阻塞状态(blocked)
当Monitor的Owner将临界区中代码执行完毕后,Owner便会被清空,此时EntryList中处于阻塞状态的线程会被叫醒并竞争,此时的竞争是非公平的。
如和判断对象是否跟monitor绑定?
1. 对象在使用了synchronized后与Monitor绑定时,会将对象头中的Mark Word置为Monitor指针。
2. 每个对象都会绑定一个唯一的Monitor,如果synchronized中所指定的对象(obj)不同,则会绑定不同的Monitor
3. synchronized 必须是进入同一个对象的 monitor 才有上述的效果不加 synchronized 的对象不会关联监视器,不遵从以上规则。
2.Syn进阶
再次看一下对象中markword结构:
2.1.轻量级锁:
轻量级锁是java用来优化Monitor这类重量级锁的:
轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是synchronized
,假设有两个方法同步块,利用同一个对象加锁。
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
加锁流程:
1.每次指向到synchronized代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的Mark Word和对象引用reference。
2.让锁记录中的Object reference指向对象,并且尝试用cas(compare and sweep)替换Object对象的Mark Word ,将Mark Word 的值存入锁记录中。
1 3.如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态01,如下所示
4.如果cas失败,有两种情况:
如果是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段
如果是自己的线程已经执行了synchronized进行加锁,那么那么再添加一条 Lock Record 作为重入的计数,如图:
5,锁消除:当线程退出synchronized代码块的时候,
如果获取的是取值为 null 的锁记录 ,表示有重入,这时重置锁记录,表示重入计数减一;
如果获取的锁记录取值不为 null,那么使用cas将Mark Word的值恢复给对象
成功则解锁成功
失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2.2.锁膨胀
如果在尝试加轻量级锁的过程中,cas操作无法成功,这时有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。
1.当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
2.这时 Thread-1 加轻量级锁失败,进入锁膨胀流程:
a:为对象申请Monitor锁,让Object指向重量级锁地址,然后自己进入Monitor 的EntryList 变成BLOCKED状态;此时在Obj上的锁已经变成了重量级锁,thread0 解锁的时候会用重量解锁的解锁过程。
3.锁消除:当Thread-0 推出synchronized同步块时,使用cas将Mark Word的值恢复给对象头,失败,那么会进入重量级锁的解锁过程,即按照Monitor的地址找到Monitor对象,将Owner设置为null,唤醒EntryList 中的Thread-1线程
2.3.自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁。
1.自旋成功:
2.自旋失败:
自旋会占用 CPU 时间(一直占用CPU来访问对象,查询是否释放了🔒锁),单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 之后不能控制是否开启自旋功能。
2.4.偏向锁:(用于优化轻量级锁重入)
在轻量级的锁中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,也需要执行CAS操作,这是有点耗时的,所以jdk6之后,引入了偏向锁:
只有第一次使用CAS时将对象的Mark Word头设置为入锁线程ID,之后这个入锁线程再进行重入锁时,发现线程ID是自己的,那么就不用再进行CAS了;
偏向状态:
1.Normal:一般状态,没有加任何锁,前面62位保存的是对象的信息,最后2位为状态(01),倒数第三位表示是否使用偏向锁(未使用:0)
2.Biased:偏向状态,使用偏向锁,前面54位保存的当前线程的ID,最后2位为状态(01),倒数第三位表示是否使用偏向锁(使用:1)
3.Lightweight:使用轻量级锁,前62位保存的是锁记录的指针,最后两位为状态(00)
4.Heavyweight:使用重量级锁,前62位保存的是Monitor的地址指针,后两位为状态(10)
撤销偏向锁;
1.调用hashcode方法:当调用对象的hashcode方法的时候就会撤销这个对象的偏向锁,因为使用偏向锁时没有位置存hashcode
的值了;
2.其它线程使用对象:
3.调用 wait/notify:会使对象的锁变成重量级锁,因为wait/notify方法之后重量级锁才支持。
批量重偏向:
会使对象的锁变成重量级锁,因为wait/notify方法之后重量级锁才支持
批量重偏向
如果对象被多个线程访问,但是没有竞争,这时候偏向了线程一的对象又有机会重新偏向线程二,即可以不用升级为轻量级锁;
重偏向会重置Thread ID;
当撤销超过20次后(超过阈值),JVM会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至加锁线程。
批量撤销
当撤销偏向锁的阈值超过40以后,就会将整个类的对象都改为不可偏向的;
3.wait/notify 方法:
再看一下monitor的原理图:
锁对象调用wait方法(obj.wait),就会使当前线程进入WaitSet中,变为WAITING状态。
处于BLOCKED和WAITING状态的线程都为阻塞状态,CPU都不会分给他们时间片。但是有所区别:
BLOCKED状态的线程是在竞争对象时,发现Monitor的Owner已经是别的线程了,此时就会进入EntryList中,并处于BLOCKED状态;
WAITING状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了wait方法而进入了WaitSet中,处于WAITING状态;
BLOCKED状态的线程会在锁被释放的时候被唤醒,但是处于WAITING状态的线程只有被锁对象调用了notify方法(obj.notify/obj.notifyAll),才会被唤醒。
注:只有当对象被锁以后,才能调用wait和notify方法;
3.1.Wait与Sleep的区别
不同点
1.Sleep是Thread类的静态方法;Wait是Object的方法,Object又是所有类的父类,所以所有类都有Wait方法。
2.Sleep在阻塞的时候不会释放锁,而Wait在阻塞的时候会释放锁
3.Sleep不需要与synchronized一起使用,而Wait需要与synchronized一起使用(对象被锁以后才能使用)
相同点
阻塞状态都为TIMED_WAITING
3.2.什么时候使用wait 和 sleep?
当线程不满足某些条件,需要暂停运行时,可以使用wait。这样会将对象的锁释放,让其他线程能够继续运行。
如果此时使用sleep,会导致所有线程都进入阻塞,导致所有线程都没法运行,直到当前线程sleep结束后,运行完毕,才能得到执行。
简而言之:wait方法只有对象自己的线程等,不用别人等他,sleep需要所有人一块睡。
注意:当有多个线程在运行时,对象调用了wait方法,此时这些线程都会进入WaitSet中等待。如果这时使用了notify方法,可能会造成虚假唤醒(唤醒的不是满足条件的等待线程),这时就需要使用notifyAll方法。