java对象头
以32位虚拟机为例
普通对象(klass word类型指针)
数组对象
其中Mark world结构为
其中1代表启用了偏向锁,0表示没启用
Monitor
被翻译为监视器或管程(syschronized底层实现原理)
每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象的Mark word中被设置指向Monitor对象的指针
Monitor的结构如下
流程:
- 刚开始Monitor中Owner为null
- 当Thread执行synchronized(obj)就会将Owner置为Thread2,Monitor中只能又一个Owner,同事obj对象中mark wrod变为指向monitor的指针
- 在Thread-2上锁的过程中,如果Thread3,Thread-4也来执行同步代码块,就会进入EntryList BLOCKED
- Thread-2执行完同步代码块的内容,然后唤醒EntryList等待的线程来竞争锁,竞争的是非公平的
- 图中waitSet中的Thread-0, Thread-1是之前获得过锁,但条件按不满足进入WAITING状态的线程
注意:1. synchronized必须是进入同一个对象的monitor才有上述效果
- 不加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,将Mark Word的值存入锁记录
- 如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下
- 如果cas失败,有两种情况
- 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了synchronized锁冲入,那么再添加一条Lock Record作为重入的计数
- 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时有重入,这时重置锁记录,表示重入计数减1
- 当退出synchronized代码块(解锁时)所记录的值不为null,这时使用cas讲Mark Word的值回复给对象
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
这时Thread-1加轻量级锁失败,进入锁膨胀过程
- 即为Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后进入Monitor的EntryList BLOCKED
当 Thread-0退出同步块解锁时,使用cas讲Mark Word的值恢复给对象头,失败,这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功,即此时持锁线程已经退出了同步块,释放了锁,这时当前线程就可以避免阻塞
- 在java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性很高,就多自旋几次,反之就少自旋甚至不自旋,总之,比较智能
- 自旋会占用cpu时间,单核cpu自旋就是浪费,多喝cpu自旋才能发挥优势
- java7之后不能控制是否开启自旋功能
偏向锁
轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作
java6中引入了偏向锁来进一步优化:只有第一次使用CAS讲线程ID设置到对象的MARK WORD头,之后发现这个线程id是自己就表示没有竞争,不用重新cas,以后只要不发生竞争,这个对象就归该线程所有
- 一个对象开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后三位为101,这是它的thread/epoch/age都为0
- 偏向锁是默认延迟的,不会再程序启东时立即生效,如果想避免延迟,可以加vm参数 -XX:BiasedLockingStartupDelay=0来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后三位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值
撤销-调用对象hashCode
调用了对象的hashcode,但偏向锁的对象markword中存储的是线程id, 如果hashCode会导致偏向锁被撤销
- 轻量级锁会在锁记录中记录hashcode
- 重量级锁会在Monitor中记录hashcode
交错的不同线程共同申请该锁则会升级为轻量级锁,有竞争的则升级为重量级锁
此外调用wait/notify也会升级为重量级锁
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这是偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的ThreadID
当撤销偏向锁预支超过20次后,jvm会觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
批量撤销
当撤销偏向锁阈值超过40次后,jvm会这样觉得,自己确实偏向错了,根本就不该偏向,于是整个累的所有对象都变为不可偏向的,新建的对象也是不可偏向的
锁消除
如果一个对象被发现只能从一个线程被访问到,那么这个对象的操作可以不考虑同步
原理值wait/notify
- Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
- BLOCKED和WAITING的线程都处于阻塞状态,不占用时间片
- BLOCKED会在Owner线程释放锁时唤醒
- WAITING线程会在Owner调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争
保护性暂停
join底层即为使用wait的保护性暂停来实现
class GuardedObject {
private Object response;
public Object get(long timeout) {
synchronized (this) {
long begin = System.currentTimeMillis();
long passTime = 0;
while(response == null){
long waitTime = timeout - passTime;
if(passTime >= timeout){
break;
}
try {
this.wait(waitTime);
} catch (InterruptedException e){
e.printStackTrace();
}
passTime = System.currentTimeMillis() - begin;
}
return response;
}
}
}
Park & Unpark
他们是LockSupport类中的方法
//暂停当前线程
LockSupport.park();
//恢复某个线程的运行
LockSupport.unpark(暂停线程对象);
与Object的wait¬ify相比
- wait, notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必
- park&unpark是以线程为单位来阻塞和唤醒线程的,而notify只能随机唤醒一个线程
- park和unpark可以先unpark,而wait¬ify不能先notify
原理之park和unpark
每个线程都有自己的一个Parker对象,由三部分组成_counter,_cond,_mutex打个比喻