本文参考 黑马Java并发编程
Synchronized
什么是加锁
拿上厕所锁门举个例子吧:
- 有一天,thread1正在上厕所
- thread2也来了,它也想上厕所,但厕所只有一个,于是它冲进去和thread2抢了起来...
- 又一天,thread1去上厕所,吸取上次教训,它这次把门锁起来了
- thread2又来了,它又想上厕所,但厕所只有一个,于是它冲进...欸上锁了冲不进去,所以只好在门外等着了
把厕所对应数据A,上厕所对应改数据,如果thread1刚把 A 改成 10,thread2就冲进来把 A 改成了 20,那一会thread1 拿起 A 参与别的运算,不就错了嘛,这时我们就需要加锁解决了。
那锁长啥样,加锁的流程?👇
Monitor原理
每个Java对象都可以关联一个monitor对象(操作系统提供),如果使用Synchronized给对象上锁之后,该对象头的MarkWord中就被设置指向Monitor对象的指针
对象头结构:
Monitor结构:
根据结构,我们来看看上锁(重量级锁)流程:
-
以下例子的上锁操作:
synchronized(obj) { ... }
-
thread1 来了,对obj上锁:把obj的锁状态位改成
10
,并把 hashcode、age 这些信息保存到一个monitor的owner中,空出来的位置放一个地址指针,指向这个monitor,然后让这个monitor的owner指针指向自己,上锁成功! -
thread2 来了,要对obj上锁,一看Markword指向了一个monitor,而且这monitor有owner,说明已经有线程占着锁了!行吧,我到
EntryList
等着(阻塞)去吧 -
thread3 来了,与thread2一样...
-
thread1 总算忙完了,开始解锁:先把放在monitor的owner那的hashcode啥的信息拿回来,再唤醒EntryList里的线程,让thread2和thread3抢owner去
自旋优化
在上面的加锁过程中我们可以看到:如果发生了锁被占用了,那么新到的线程要进入到EntryList
,变为阻塞状态,这时CPU会发生线程上下文切换,执行别的线程任务;
如果thread2刚进入阻塞状态,thread1就解锁了,那我们又得唤醒线程,上下文切换;如果我们能让thread2啥也不干等上一小会,那就不用做出这些额外开销了!这便是自旋优化的核心思想,当然这只在多核CPU下有意义。
轻量级锁优化
上面说的是重量级锁,也就是加锁需要申请并关联一个Monitor对象,这一系列操作是比较重量级的;
但如果thread1和thread2每次用锁的时间是错开的,那是不是就可以不用申请Monitor来存放阻塞线程呢?
于是jdk1.6中引入了轻量级锁优化:
- thread1 加锁时,先在线程栈帧创建一个 Lock Record 对象,并且把它里面的内容和obj的Markword信息进行cas交换,使得obj的锁状态为
00
- 如果cas交换成功:则该obj的markword记录了该线程的锁记录地址,上锁成功
- 如果cas交换失败:
① 是该线程再次获取锁,即重入;这种情况下加多一条LockRecord,内容为null,用于计数
② 是别的线程来获取锁,即发生了锁竞争,这是必须进行锁膨胀 - 锁膨胀:即轻量级锁升级为重量级锁,为当前obj申请一个Monitor对象,将引起竞争的线程加入该Monitor的
EntryList
,并Monitor对象的owner设置为锁记录对象
- 解锁:
① 如果锁记录值为null,则删去一条锁记录
② 如果锁记录值不为null,则尝试使用cas交换恢复obj的MarkWord- 成功:解锁成功;
- 失败:说明发生了锁膨涨,进入重量级锁解锁过程
偏向锁优化
轻量级锁效率已经挺不错的了,还能不能更好呢?
欸,能!
我们可以发现,轻量级锁在上锁的过程中,每次重入仍需要执行CAS操作,如果一段时间内只有thread1用这个锁,岂不是多此一举?
所以,我们默认在thread1获取锁的时候,将线程ID写入MarkWord中,并修改偏向状态为101(其实默认状态就是101),那么下次thread1重入获取锁时,就会判断该线程ID是不是我的,是就直接开始执行代码;
这里我们需要关注一些会撤销obj偏向状态的情况:
- 调用obj.hashcode(),MarkWord存放的是线程ID,放不了hashcode
- 其它线程使用obj加锁,会升级为轻量级锁
- 调用wait/notify方法
- 批量重偏向:当撤销偏向锁阈值超过 20 次后,会批量重偏向到撤销的线程
- 批量撤销:当撤销偏向锁阈值超过 40 次后,该类型的所有对象(包括以后创建的)批量变为不可偏向状态
必要条件:锁只有一个线程在用的情况下;如果有其它线程用过这个锁,哪怕没有竞争也要升级为轻量级锁!
锁消除
经过逃离分析分析出,上锁对象的作用域在方法内部,每次上锁的对象都不一样,简而言之上锁等于没上,于是会消除这段上锁代码
举例:
public void test(){
Object obj = new Object();
Synchronized(obj){
hello...
}
}
很明显可以看出,这个锁没有意义,于是会被优化消除(JIT即时编译器来做)
Q.E.D.