参考于黑马的视频和文档
对象头格式
重量级锁
Thread2进入到临界区中的时候,会把object的对象头中的MarkWord设置为指向Monitor对象的指针
- 开始的时候Monitor的owner为null
- Thread2执行到临界区的时候,就会会把Monitor的owner设置为Thread2,Monitor只能有一个owner
- Thread2上锁的过程中,如果Thread2和Thread3也进入到临界区执行,就会进入EntryList中阻塞(BLOCKING)
注意:
- 轻量级锁的时候,obj原来的markword存储在lock Record中
- 重量级锁的时候,原markword则存储在monitor对象中
上锁和解锁
- 如果遇到异常:
字节码里面有一个exception table异常表,里面会存储监控的范围,如果遇到异常,会跳到19行(如图所示)先把异常对象存储起来,然后进行解锁操作,再把异常抛出。
轻量级锁和锁膨胀
-
需要注意:此时
-
record保存的是hashcode数据;
-
object原来的markword则保存的是指向record的指针+锁标志位
-
00轻量级锁定状态,01是可偏向/偏向锁状态
锁自旋
自旋失败:
注意:
- 自旋默认是10次,可以使用-XX:PreBlockSpin更改自旋次数
- JDK6之后是自适应自旋,如果自旋成功,那下次获取锁会多自旋几次;相反,如果自旋很少成功获得锁,那么可能下次直接省略自旋的过程,避免浪费处理器资源。
偏向锁
偏向锁是对轻量级锁的优化
当只有一个线程的时候,轻量级锁每次重入锁的时候都会生成一个新的LockRecord空间,然后进行CAS操作替换MarkWord,最后为null,然后继续执行。(参考上面),这样会影响性能
如何实现
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现
这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101(默认为偏向状态),这时它的
thread、epoch、age 都为 0 - 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -
XX:BiasedLockingStartupDelay=0
来禁用延迟 - 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
age 都为 0,第一次用到 hashcode 时才会赋值
偏向状态的时候,MarkWord存储在哪里呢?(参考深入理解JVM)
细心的读者看到这里可能会发现一个问题:当对象进人偏向状态的时候,Mark Word大部分的空间(23个比特)都用于存储持有锁的线程ID了,这部分空间占用了原有存储对象哈希码的位置,那原来对象的哈希码怎么办呢?
在Java 语言里面一个对象如果计算过哈希码,就应该一直保持该值不变(强烈推荐但不强制,因为用户可以重载 hashCode0 方法按自己的意愿返回哈希码),否则很多依赖对象哈希码的 API都可能存在出错风险。而作为绝大多数对象哈希码来源的 Object::hashCodeo方法,返回的是对象的一致性哈希码(Identity Fash Code),这个值是能强制保证不变的,它通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的哈希码值永远不会再发生改变。因此,当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨账为重量级锁。在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的 ObjectMonitor 类里有字段可以记录非加锁状态(标志位为“01”)下的Mark Word,其中自然可以存储原来的哈希码。
偏向锁可以提高带有同步但无竞争的程序性能,但它同样是一个带有效益权衡 (Trade Off)性质的优化,也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数-XX:UseBiasedLocking来禁止偏向锁优化反而可以提升性能。
偏向锁撤销
-
会在调用hashcode的时候撤销
-
竞争时撤销,并且锁升级