对于java锁升级,很多人都停留在比较浅层的表面理解,一定程度下也许够用,但如果学习其中的细节,我们更好地理解多线程并发时各种疑难问题的应对方式!
因此我将锁升级过程中可能涉及的大部分细节或者疑问都整合成了一篇文章,希望你能直接在这篇文章中,搞懂你当年学习这块时遗留的所有疑问。
为什么说线程切换会很慢?
所谓的用户态和内核态之间的切换仅仅是一方面, 并非全部, 更关键的在于, 多CPU的机器中,当你切换了线程,意味着线程在原先CPU上的缓存也许不再有用, 因为当线程重新触发时,可能在另一个CPU上执行了。
正因如此,不建议没事就挂起线程, 让线程自旋会比挂起后切换CPU好很多。
正与基于这一点,才有了后面的sync锁升级机制,理解了为什么要锁升级,才能逐步理解锁升级过程,
对象头中的mark-word
java每个对象的对象头中, 都有32或者64位的mark-word。
mark-word是理解锁升级过程的重要部分,且后面的锁升级过程都会涉及,因此这里会进行一个非常详细的解释。这部分只对一个对象必有的属性做解释(即一般不会随着锁状态变化而消失的属性)。对于各锁状态独有的属性,会在锁升级过程中做详细的解释。
作者:华为云开发者联盟
链接:https://zhuanlan.zhihu.com/p/537852119
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
锁状态标志位 +偏向锁标记位(2bit + 1bit)
除了markword中的2位锁状态标志位, 其他62位都会随着锁状态标志位的变化而变化。
这里先列出各锁状态标志位代表的当前对象所用锁的情况。后面会详细解释各种锁的含义和运行原理。
- 锁状态标志位为01: 属于无锁或者偏向锁状态。因此还需要额外的偏向锁标记位1bit来确认是无锁还是偏向锁
- 锁状态标志位为00: 轻量级锁
- 锁状态标志位为10: 重量级锁
- 锁状态标志位为11: 已经被gc标记,即将释放
为什么无锁/偏向锁的标志位是01,而轻量级锁的标志位是00?
即按理说,无锁是锁状态的初始情况,为什么标志位不是从00开始?
个人查询到的一个解释,是因为 轻量级锁除了锁标志位外,另外62位都是一个指针地址。
如果将轻量级锁标志位设置为00, 那么在判断标志位为00后, m无需再额外做一次markWord>>2的操作,而是直接将markWord拿来当作地址使用即可!
可以从这里看到jvm的设计者还是非常细节的,并没有随意地定义各状态的标志位
hashcode(31bit)
哈希code很容易理解,将对象存储到一些map或者set里时,都需要hashcode来确认插入位置。
但markword里的hashcode,和我们平时经常覆写的hashCode()还是有区别的。
markword中的hashcode是哪个方法生成的?
很多人误以为,markword中的hashcode是由我们经常覆写的hashcode()方法生成的。
实际上, markword中的hashcode只由底层 JDK C++ 源码计算得到(java侧调用方法为 System.identityHashCode() ), 生成后固化到markword中,如果你覆写了hashcode()方法, 那么每次都会重新调用hashCode()方法重新计算哈希值。
根本原因是因为你覆写hashcode()之后,该方法中很可能会利用被修改的成员来计算哈希值,所以jvm不敢将其存储到markword中。
因此,如果覆写了hashcode()方法,对象头中就不会生成hashcode,而是每次通过hashcode()方法调用
markword中的hashcode是什么时候生成?
很容易误以为会是对象一创建就生成了。
实际上,是采用了延迟加载技术,只有在用到的时候才生成。毕竟有可能对象创建出来使用时,并不需要做哈希的操作。
hashcode在其他锁状态中去哪了?
这个问题会在后面锁升级的3个阶段中,解释hashcode的去向。其他的例如分代年龄同理。
gc分代年龄(4bit)
在jvm垃圾收集机制中, 决定年轻代什么时候进入老年代的根据之一, 就是确认他的分代年龄是否达到阈值,如下图所示。
分代年龄只有4bit可以看出,最大值只能是15。因此我们设置的进入老年代年龄阈值 -XX:MaxTenuringThreshold 最大只能设置15。
cms_free
在无锁和偏向锁中,还可以看到有1bit的cms_free。
实际上就是只有CMS收集器用到的。但最新java11中更多用的是G1收集器了,这一位相当于不怎么常用,因此提到的也非常少。
从上述可以看出, 只有锁状态标记位、 hashcode、 分代年龄、cms_free是必有的, 但是从markword最初的示意图来看,