最近研究学习了Java1.6对锁的优化技术,重点就是偏向锁,轻量级锁。
如下是几种锁之间的状态转换,清晰明了:
image.png
结合网上学习的资料,总结了一下自己对锁优化的理解,只有自己把问题重述明白了,才是真正的掌握了知识。
Jvm锁优化的原理
synchronized在1.6之前完全依赖操作系统底层互斥量来完成同步操作,每次调用都需要程序由用户态进入内核态,然后内核态回到用户态,两种态之间的进入进出需要进行大量的寄存器以及内存复制操作,很是费事费力,因此也是重量级锁。
所以锁优化的目的和原理就是要尽量消除或者减少这种内核态和用户态之间的相互转换
,也就是说在某些适合的场景下,通过使用用户态下的锁机制来避免进入内核级锁竞争来提高效率。
如下是各种锁优化技术的适用场景。
image.png
image.png
偏向锁优化
适用于只有一个线程访问同步代码块的场景。这种情况下,通过CAS操作来将判断和(竞争)更新对象头中的ThreadId。如果ThreadId是本线程,则直接进入同步代码块执行。这样,无需进入和竞争内核级锁,原线程无间断继续执行;如果ThreadId不是本线程或者CAS竞争操作失败,则锁升级为轻量锁。
轻量锁
适用于同步代码块执行非常快,线程竞争不是很激烈的情况。这种情况下,通过CAS操作判断和(竞争)更新对象头中的锁指针,使其指向本线程中复制的对象头副本。如果更新成功,则竞争轻量锁成功,线程无需进入和竞争内核级锁,无间断继续执行同步代码块;如果CAS竞争操作失败,说明有其他线程在竞争,此时线程可以先自旋一段CPU时间之后再次CAS竞争操作,如果竞争成功,则说明线程竞争不是很激烈,或者同步代码块执行非常快,线程继续无间断执行同步代码块,无需借助内核级重量锁。如果自旋后CAS竞争继续失败,说明竞争非常激烈,则升级为重量级内核锁。
可以看到,偏向锁和轻量锁都是在不间断原执行线程的情况下,通过CAS操作来同步多线程之间的竞争,避免用户态和内核态切换。不同于重量锁,重量锁是由用户态进入内核态竞争内核级锁,由此中断了原线程的执行,使原线程可能进入睡眠状态,由此,多线程之间可能多次重复的睡眠唤醒睡眠唤醒,效率低下。
CAS操作
compareAndSwap,比较并替换,是一种实现并发算法时常用到的技术
CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B
比如你要操作一个变量,他的值为A,你希望将他修改为B,这期间不会进行加锁,当你在修改的时候,你发现值仍旧是A,然后将它修改为B,如果此时值被其他线程修改了,变成了C,那么将不会进行值B的写入操作,这就是CAS的核心理论,通过这样的操作可以实现逻辑上的一种“加锁”,避免了真正去加锁
以上是CAS操作的概念。具体到CAS落实到Jvm锁优化中来,其中对应的预期值A和目标值B,就是对象头Mark Word。
对象头Mark Word
如下是对象头Mark Word。存储对象自身的运行时数据,如:Hash Code,GC 分代年龄、锁信息。这部分数据在32位和64位的 JVM 中分别为 32bit 和 64bit。考虑空间效率,Mark Word 被设计为非固定的数据结构,以便在极小的空间内存储尽量多的信息,32bit的 Mark Word 如下图所示:
image.png
初看对象头的状态图片,可能会有疑惑,因为对象头存储对象自身的运行时数据,如:Hash Code,GC 分代年龄、锁信息。但是这是无锁状态下存储的内容,但是当有了锁之后,这些信息就都换成了锁信息,原本的信息去哪了?我们运行程序不能没有对象的HashCode,GC等信息啊。
其实,线程在进入到同步代码块的时候,JVM 会先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象当前 Mark Word 的拷贝(官方称为 Displaced Mark Word),owner 指针指向对象的 Mark Word。此时堆栈与对象头的状态如图所示:
image.png
所以这里会形成一个双向指针,栈桢里的指针owner指向对象头地址,对象头里的指针指向栈桢里的锁记录地址。可以参考这篇博客的轻量级锁执行过程锁升级过程
来深入理解,锁对象时,只有当对象头里的内容和栈桢里的对象头拷贝一致才会CAS成功并将栈桢锁记录地址CAS到对象头里实现占用锁。当释放锁时,又需要CAS操作将对象头拷贝复制回对象里去,这里要求使用CAS,为啥呢?因为在线程持有锁的期间,如果有其他线程竞争锁,会将对象头里的mark word改为指向内核锁的指针,这里的指针就变了。所以这个owner必须指向对象,否则后面就找不到对象了。此时轻量级锁升级为重量级锁,对象头副本只存在原持有轻量级锁的线程中,并且原线程现在持有的是重量级锁了,它释放锁后会将对象头拷贝复制回去,并唤醒其他线程重新竞争重量级锁,谁抢到重量级锁谁拷贝对象头。
对于偏向锁,轻量锁,CAS操作的值就是这个Mark Word。首先将原Mark Word复制到当前线程栈中,然后将原Mark Word内容更新为锁的信息。通过逻辑锁CAS,判断原Mark Word中的内容是否与栈中的Mark Word副本内容相同,相同则CAS更新成功,不同则说明有竞争,更新失败。当然,具体的CAS操作还会判断原Mark Word中保存的锁指针是不是本线程栈中的Mark Word副本地址。这就是锁机制中的CAS操作。
为什么不直接摒弃内核级重量锁
有的人可能会问,为什么不用轻量级锁直接替代重量级锁?因为轻量级锁理论上完全可以用于多线程竞争。但是为什么有这么高效的锁还要升级为重量锁呢?原因就是,使用场景不同。轻量级锁适用于同步代码块执行非常快,线程竞争不是很激烈的情况。通过一定次数的自旋来避免陷入重量锁。但是如果竞争非常激烈的话,自旋操作就很浪费了,这种情况下的自旋成本可能极大的超过了重量锁的成本,所以还是直接升为重量锁,避免大量无用自旋,线程竞争重量锁,竞争成功则执行同步代码,失败则睡眠。
JVM默认会在启动后4秒才会启动偏向锁(启动过程中大量创建对象,为了防止竞争,直接用重量级锁)
一定要线程竞争才会出现锁升级吗?不是的,调用对象的hashcode也会造成锁升级。
hashcode与偏向锁1
hashcode与偏向锁2
网友评论