写在前面:这篇关于锁膨胀的文章,是我学习过程中,结合 《Java并发编程的艺术》 以及多篇文章整理而成,其中部分内容感觉仍然没有理解到位,后续有新的理解,会附注在后面。
在并发编程中, synchronized
一直被称为重量级锁,但是随着 JDK1.6 对 synchronized 进行了各种优化之后,有些情况下它就并不那么重了。
从 JDK 1.6 开始引入了偏向锁和轻量级锁,从而让锁拥有了四个状态,即: 无锁状态
、 偏向锁状态
、 轻量级锁状态
、 重量级锁状态
。
简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景:
只有一个线程进入
临界区; 交替进入
临界区; 同时进入
临界区。 在了解锁的时候首先需要理解java的对象头
锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit,如下图
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构,如下图
上图中展示了 Java 锁( synchronized
) 的四种状态:
偏向锁状态(biasble):锁标志位为 01
这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
偏向锁的核心思想是偏向于第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
工作流程是这样的,如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构也变为偏向锁结构(上述表格中的最后一行,请返回查看),并且使用 CAS 操作将当前线程的 ID 记录到对象头的 Mark Word 中。当该线程再次请求锁时,无需再做任何同步操作,连 CAS 都不需要了,只需要检查对象头中 Mark Word 里的 Thread ID 是否是此线程的 ID。
如果是的话就说明此线程获取锁成功。
如果不是的话,那么还需检查 Mark Word 中偏向锁的标识是否设置成 1(就是之前我说的那个 1bit 标识位,1表示当前是偏向锁),如果没有设置,则使用 CAS 竞争锁;如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。
偏向锁使用了一种 等到竞争出现才释放锁
的机制,所以当其他线程尝试竞争偏向锁时,
持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(Safe point 安全点这个概念对了解 Java 虚拟机的读者应该并不陌生,GC 的工作也需要在安全点进行,在这个时间点上没有正在执行的字节码)。
它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 Mark Word 要么重新偏向于其他线程,要么恢复到无锁(此时锁降级为无锁状态)或者标记对象不适合作为偏向锁(此时升级为轻量级锁),最后唤醒暂停的线程。
偏向锁从 JDK1.6 以后开始是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用 JVM 参数来关闭延迟:
-XX:BiasedLockingStartupDelay = 0
如果你确定应用程序里所有的锁通常情况下都处于竞争状态,可以通过 JVM 参数来关闭偏向锁:
-XX:-UseBiasedLocking = false
此时程序默认会进入轻量级锁状态。
线程在执行同步块之前,JVM 会先在当前线程的栈帧中创建用于存储锁记录(Lock Record)的空间,并将对象头中的 Mark Word (此时 Mark Word 处于默认存储结构即无锁状态)复制到锁记录(Lock Record)中,官方称为 Displaced Mark Word 。
此时的 Mark Word:
将其拷贝到当前线程的栈帧中的锁记录(Lock Record)的空间,具体来说是拷贝到下图中左侧当前线程的栈帧中的 displaced hdr 的位置。
如下所示,左侧为当前线程的栈帧,它是线程私有的,右侧即为 Mark Word。
然后线程尝试使用 CAS 将对象头的 Mark Word 替换为指向锁记录(Lock Record)的指针,将 owner 指针更改为指向对象头的 Mark Word 指针(这里看不明白的请返回 Java 对象头那一小节中查看 Mark Word 状态变化表那里,结合轻量级锁的存储布局来理解)。
如果成功,当前线程获得锁,并且对象头的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。,如果失败,表示其它线程竞争锁,当前线程便尝试使用自旋来获取锁。
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行。
否则说明多个线程竞争锁,那轻量级锁就不再有效,要膨胀为重量级锁。此时锁标志的状态变为 ”01“ ,Mark Word 存储的就是指向重量级锁(互斥量)的指针(结合对象头那 Mark Word 状态变化表那里,结合重量级锁的存储布局来理解)。
轻量级锁解锁时,会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头.
如果成功,则表示没有竞争发生,整个同步过程就完成了。
如果失败,表示当前锁存在竞争(由上面的轻量级锁加锁过程我们这里应该知道,此时锁已经膨胀成重量级锁了)。此时释放锁,并唤醒被挂起的线程。
上图跟下面这个解析基本相同:
一个对象刚开始实例化的时候,没有任何线程来访问它的时候,它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,( 偏向锁就是这个时候升级为轻量级锁的 )。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
总结一下:
偏向锁
使用 CAS 操作 + 检查 Mark Word 中的 Thread ID 来获取锁,所以它是一种 乐观锁
。 轻量级锁
使用 CAS 操作 + 锁自旋来获取锁,所以它也是一种 乐观锁
。 重量级锁
是 悲观锁
。 在 JDK1.6 对 synchronized 进行了优化后,它变得没那么重量级了。(对 synchronized 的优化还不仅仅如此,还有诸如自旋锁、自适应自旋锁、锁粗化、锁消除等优化)
《Java并发编程的艺术》
jvm从轻量级锁膨胀到重量级锁是在什么时候发生的?
JAVA锁的膨胀过程
synchronized 锁优化(二):锁的状态及锁膨胀