synchronized 锁的优化过程:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
一、不同锁对象的状态表示(需要了解 Java 对象头)
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
二、关于 Lock Record(锁记录)
https://www.jianshu.com/p/fd780ef7a2e8
当字节码解释器执行 monitorenter 字节码轻量级锁锁住一个对象时,就会在获取锁的线程的栈上显式或者隐式分配一个 Lock Record 空间。
三、偏向锁
https://www.cnblogs.com/javaminer/p/3892288.html
在 JDK 1.6 中引入,默认启用,且会延迟启动,关闭延迟:-XX:BiasedLockingStartupDelay=0,关闭偏向锁:-XX:-UseBiasedLocking=false,默认会进入轻量级锁。
为了消除数据在无竞争(大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得)情况下的同步原语,提高程序的运行性能(消除这个线程锁重入(CAS)的开销)。
1.获取锁:
OpenJDK8 的 HotSpot 源码(markOop.hpp)中关于检测一个对象是否处于可偏向状态的源码
// 返回 true 时代表 markword 的可偏向标志 bit 位为 1 ,且对象头末尾标志为 01。 bool has_bias_pattern() const { return (mask_bits(value(), biased_lock_mask_in_place) == biased_lock_pattern); } // 返回 NULL 时代表对象 Mark Word 中 bit field 域存储的 Thread Id 为空。 JavaThread* biased_locker() const { assert(has_bias_pattern(), "should not call this otherwise"); return (JavaThread*) ((intptr_t) (mask_bits(value(), ~(biased_lock_mask_in_place | age_mask_in_place | epoch_mask_in_place)))); } // 检测对象是否处于可偏向状态 bool is_biased_anonymously() const { return (has_bias_pattern() && (biased_locker() == NULL)); }
若对象处于可偏向状态
- JVM 使用 CAS 操作尝试将当前线程 ID 更新到对象头中。
- 更新成功(判断对象是否获得偏向锁的条件:mark 字段后 3 位是101,thread 字段跟当前线程相同,epoch 字段跟所属类的 epoch 值相同),执行同步代码块内容。
- 更新失败,表示该锁存在竞争且这个时候另外一个线程已获得偏向锁所有权。需要撤销偏向锁,改为轻量级锁。
若对象处于已偏向状态
- 则检测 MarkWord 中存储的 Thread ID 是否等于当前 Thread ID 。
- 相等, 证明该线程已经获取到偏向锁, 可直接继续执行同步代码块。
- 不等, 证明该对象目前偏向于其他线程, 需要撤销偏向锁,改为轻量级锁。
关于撤销偏向锁(Revoke Bias)
当到达全局安全点(safepoint,此时间点, 没有线程在执行字节码,可以 stop the word)时获得偏向锁的线程被挂起,撤销偏向锁,改为轻量级锁,然后被阻塞在安全点的线程继续执行同步代码。
通过 MarkWord 中已经存在的 Thread Id 找到成功获取了偏向锁的那个线程, 然后在该线程的栈帧中建立 Lock Record 空间,然后就是轻量级锁获取锁的过程了。
2.释放锁:
一般锁在执行完同步代码块后, 都会有释放锁的操作, 而偏向锁并没有直观意义上的“释放锁”操作。
同步代码块执行完毕后只需要测试锁对象上的偏向锁模式是否还存在,如果存在则解锁成功,不需要额外的操作。
不会尝试将 Mark Word 中的 Thread ID 赋回原值 0 。这样做的好处是: 如果该线程需要再次对这个对象加锁,而这个对象之前一直没有被其他线程尝试获取过锁,依旧停留在可偏向的状态下, 即可在不修改对象头的情况下, 直接认为偏向成功。
关于重偏向(Rebias)
一个对象先偏向于某个线程, 执行完同步代码后, 另一个线程就不能直接重新获得偏向锁吗?
https://www.zhihu.com/question/56582060
答案是可以的,在方法区的每个类对象中存储着 epoch,当创建类实例对象时,实例对象中的 epoch 值来自类对象。
进入安全点时,若需要重偏向,会把类对象中 epoch 值增加,然后扫描所有持有该类实例对象的线程栈, 根据线程栈的信息判断出该线程是否锁定了该对象, 仅将改变后的 epoch 赋给被锁定的对象。
退出安全点后,当有线程需要尝试获取偏向锁时, 直接检查类实例对象中存储的 epoch 值与类对象中存储的 epoch 值是否相等, 如果不相等, 则说明该对象的偏向锁已经无效了, 可以尝试对此对象重新进行偏向操作。
疑问:什么时候需要重偏向,触发点是什么。
四、轻量级锁
轻量级锁一般(当禁用偏向锁时,执行同步代码会直接进入轻量级锁)是由偏向所升级而来。偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
1.此时对象头状态
偏向锁撤销后,对象可能处于两种状态。
- 获取偏向锁的线程若已经执行完同步代码块, 相当于原有的偏向锁已经过期无效了。此时对象被转换为不可偏向的无锁状态(未执行轻量级加锁流程)。
- 获取偏向锁的线程若未经执行完同步代码块, 偏向锁依旧有效, 此时被转换为被轻量级加锁的状态(已执行轻量级加锁流程)。
2.获取锁:
- 代码进入同步块的时,如果此同步对象没有被锁定(锁标志位为“01”),JVM 将在当前线程的栈帧中建立 Lock Record 空间,用于存储锁对象目前的 Mark Word 的拷贝(Displaced Mark Word)。
- JVM 使用 CAS 操作尝试将锁对象的 Mark Word 中除锁标志位之外的空间更新为指向 Lock Record 的指针。
- 更新成功(Mark Word 中的锁标志位会被设置为“00”),执行同步代码块内容。
- 更新失败,JVM 会检查对象的 Mark Word 是否指向当前线程的栈帧。是就说明当前线程已获取锁,可直接进入同步块继续执行。否说明该锁对象已被其他线程获取。若此时只有两个线程竞争,则该线程会自旋获取锁。
3.释放锁:
- 通过 CAS 操作来进行,如果对象的 Mark Word 仍然指向着线程的 Lock Record,那就用 CAS 操作把当前线程栈中的 Displaced Mark Word 拷贝回对象的 Mark Word 中。
- 替换成功,即释放锁完成,整个同步过程也就完成了。
- 替换失败,说明有其它线程(此时线程数 > 2)尝试过获取该锁(此时已经膨胀为重量级锁),那就直接释放锁,并唤醒被挂起(阻塞)的线程。
4.过程状态图:
https://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf
https://www.researchgate.net/publication/242536194_The_Hotspot_Java_Virtual_Machine
轻量级锁CAS操作之前堆栈与对象的状态
轻量级锁CAS操作之后堆栈与对象的状态
五、重量级锁(互斥锁)
若有两个以上的线程,那轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
https://www.cnblogs.com/jhxxb/p/10948653.html
六、锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距 | 若线程间存在锁竞争,会带来额外的锁撤销的消耗 | 只有一个线程访问同步块或者同步方法 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 若线程长时间竞争不到锁,自旋会消耗 CPU 性能 | 线程交替执行同步块或者同步方法,追求响应时间,锁占用时间很短 |
重量级锁 | 线程竞争不使用自旋,不会消耗 CPU | 线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗 | 追求吞吐量,锁占用时间较长 |
https://blog.csdn.net/lengxiao1993/article/details/81568130
https://icyfenix.iteye.com/blog/1018932
https://www.infoq.cn/article/java-se-16-synchronized
https://blog.dreamtobe.cn/2015/11/13/java_synchronized/
https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf