Synchronized锁升级过程:无锁 -> 偏向锁(CAS) -> 轻量级锁 (CAS,自适应自旋) -> 重量级锁 (悲观锁)
对象头信息
Synchronized锁对象,在对象头中标注锁信息,每一行是一种状态,对象只能有一种状态,通过锁标志位判断。
无锁
无锁状态:对象头25bit存对象hashcode,4bit存分代年龄,1bit存偏向锁(0),2bit存锁标志位(01)。顶图中第五行数据。
偏向锁
现在ThreadA和ThreadB线程一起抢锁,过程如下:
- 首先判断对象是否有锁,无锁进行CAS加锁。
这时候ThreadA和ThreadB进行抢锁,如ThreadA抢锁成功,会修改对象头信息:把ThreadId设置成自己,Epoch代表偏向时间戳,对象变化如下:
此时锁偏向ThreadA,如果ThreadA再次抢锁那么,会检查ThreadId是否是自己,如果是自己直接加锁,无需CAS,如果不是说明发现锁竞争。
偏向锁:指的就是偏向第一个加锁ThreadA,该线程是不会主动释放偏向锁的,只有当其他ThreadB尝试竞争偏向锁才会被释放。
偏向锁释放
其实就是ThreadB要操作的时候,看是否可以释放掉ThreadA的偏向锁。需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的ThreadA(达到全局安全点再暂停),然后检查持有偏向锁的ThreadA是否还活着:
- 如果ThreadA不处于活动状态,则将锁对象的MarkWord设置成无锁状态,(再指向ThreadB)。
- 如果ThreadA仍然活着,拥有偏向锁a的栈会被执行。
- 当线ThreadA不需要用到该偏向锁了,则恢复到无锁,(再指向ThreadB)。
- 如果ThreadA还要用,则和ThreadB产生竞争,标记对象不适合作为偏向锁。最后唤醒暂停的线程。
轻量级锁
如果ThreadB没有拿到锁,我们就会升级到轻量级锁,首先会在ThreadA和ThreadB都开辟一块LockRecord空间,然后把锁对象复制一份到自己的LockRecord空间下,并且开辟一块owner空间留作执行锁使用,并且锁对象的前30bit位合并,等待ThreadA和ThreadB来修改指向自己的线程,假如ThreadA修改成功,则锁对象头的前30bit位会存ThreadA的LockRecord的内存地址,并且ThreadA的owner也会存一份锁对象的内存地址,形成一个双向指向的形式。而ThreadB修改失败,则进入一个自旋状态,就是持续来修改锁对象。
重量级锁
如果说ThreadB多次自旋以后还是迟迟没有拿到锁,他会继续上告,告知虚拟机,我多次自旋还是没有拿到锁,这时我们的ThreadB会由用户态切换到内核态,申请一个互斥量,并且将锁对象的前30bit指向我们的互斥量地址,并且进入睡眠状态,然后我们的ThreadA继续运行知道完成时,当ThreadA想要释放锁资源时,发现原来锁的前30bit位并不是指向自己了,这时ThreadA释放锁,并且去唤醒那些处于睡眠状态的线程,锁升级到重量级锁。
重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
重量级锁维护了一个队列:
- Entry Set:待获得锁的线程
- The Owner:已获得锁的线程
- Wait Set:等待队列(调用wait方法)
当一个线程释放监视器时,在入口区和等待区的等待线程都会去竞争监视器,如果入口区的线程赢了,会从2号门进入;如果等待区的线程赢了会从4号门进入。只有通过3号门才能进入等待区,在等待区中的线程只有通过4号门才能退出等待区,也就是说一个线程只有在持有监视器时才能执行wait操作,处于等待的线程只有再次获得监视器才能退出等待状态。