首先简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景:
- 偏向锁:只有一个线程进入临界区;
- 轻量级锁:多个线程交替进入临界区;
- 重量级锁:多个线程同时进入临界区。
锁膨胀过程:
原理分析:
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个
线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将
对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,( 偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
一个锁对象刚刚开始创建的时候,没有任何线程来访问它,它是可偏向的,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问他的时候,它会偏向这个线程。此时线程状态为无锁状态,锁标志位为 01,此时 Mark Word 如下图::
当一个线程(线程 A)来获取锁的时,会首先检查所标志位,此时锁标志位为 01,然后检查是否为偏向锁,此时不为偏向锁,所以当前线程会修改对象头状态为偏向锁,同时将对象头中的 ThreadID 改成自己的 Thread ID,此时 Mark Word 如下图:
如果再有一个线程(线程 B)过来,此时锁状态为偏向锁,该线程会检查 Mark Word 中记录的线程 ID 是否为自己的线程 ID,如果是,则获取偏向锁,执行同步代码块。如果不是,则利用 CAS 尝试替换 Mark Word 中的 Thread ID,成功,表示该线程(线程 B)获取偏向锁,执行同步代码块,此时 Mark Word 如下图:
如果失败,则表明当前环境存在锁竞争情况,则执行偏向锁的撤销工作(这里有一点需要注意的是:偏向锁的释放并不是主动,而是被动的,什么意思呢?就是说持有偏向锁的线程执行完同步代码后不会主动释放偏向锁,而是等待其他线程来竞争才会释放锁)。撤销偏向锁的操作需要等到全局安全点才会执行,然后暂停持有偏向锁的线程,同时检查该线程的状态,如果该线程不处于活动状态或者已经退出同步代码块,则设置为无锁状态(线程 ID 为空,是否为偏向锁为 0 ,锁标志位为01)重新偏向,同时恢复该线程。若该线程活着,则会遍历该线程栈帧中的锁记录,检查锁记录的使用情况,如果仍然需要持有偏向锁,则撤销偏向锁,升级为轻量级锁。
在升级为轻量级锁之前,持有偏向锁的线程(线程 A)是暂停的,JVM 首先会在原持有偏向锁的线程(线程 A)的栈中创建一个名为锁记录的空间(Lock Record),用于存放锁对象目前的 Mark Word 的拷贝,然后拷贝对象头中的 Mark Word 到原持有偏向锁的线程(线程 A)的锁记录中(官方称之为 Displaced Mark Word ),这时线程 A 获取轻量级锁,此时 Mark Word 的锁标志位为 00,指向锁记录的指针指向线程 A 的锁记录地址,如下图:
当原持有偏向锁的线程(线程 A)获取轻量级锁后,JVM 唤醒线程 A,线程 A 执行同步代码块,执行完成后,开始轻量级锁的释放过程。
对于其他线程而言,也会在栈帧中建立锁记录,存储锁对象目前的 Mark Word 的拷贝。JVM 利用 CAS 操作尝试将锁对象的 Mark Word 更正指向当前线程的 Lock Record,如果成功,表明竞争到锁,则执行同步代码块,如果失败,那么线程尝试使用自旋的方式来等待持有轻量级锁的线程释放锁。当然,它不会一直自旋下去,因为自旋的过程也会消耗 CPU,而是自旋一定的次数,如果自旋了一定次数后还是失败,则升级为重量级锁,阻塞所有未获取锁的线程,等待释放锁后唤醒。
轻量级锁的释放,会使用 CAS 操作将 Displaced Mark Word 替换到对象头中,成功,则表示没有发生竞争,直接释放。如果失败,表明锁对象存在竞争关系,这时会轻量级锁会升级为重量级锁,然后释放锁,唤醒被挂起的线程,开始新一轮锁竞争,注意这个时候的锁是重量级锁。
学习链接:
https://github.com/farmerjohngit/myblog/issues/8
AbstractQueuedSynchronizer的介绍和原理分析:http://ifeve.com/introduce-abstractqueuedsynchronizer/
AQS 原理以及 AQS 同步组件总结:https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484832&idx=1&sn=f902febd050eac59d67fc0804d7e1ad5&source=41#wechat_redirect