Java并发编程的艺术——读书笔记(三) synchronized的实现原理与应用

第二章 Java并发机制的底层实现原理(二)

Java对象头

Java对象头中以一字宽的长度存储Mark Word,里面包含了对象的hashCode,分代年龄和锁标记位,Mark Word在32位JVM中的默认存储结构如下:

运行期间,Mark Word的数据类型会随着锁标记位的改变而改变,一共有四种情况:

锁的级别从低到高依次是,无锁,偏向锁,轻量级锁,重量级锁,其中偏向锁和轻量级锁是在Java1.6时引入的,这几个状态会随着竞争情况逐渐升级,且只能升级不能降级,下面来逐一讨论。

在此之前先解释一下CAS的概念,具体的描述会在之后的章节中提到,现在只是帮助理解下面的内容:

CAS是compare and swap的缩写,中文翻译成比较并交换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值)。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

偏向锁

大多数情况下,锁不仅不存在资源竞争,而且总是由同一个线程多次获取,为了降低开销,java引入了偏向锁,当一个线程访问同步块时获取锁,同时会在该锁的对象头中存储自己的线程ID,此后线程在进入和退出同步块只需简单测试一下该锁的Mark Word里的线程ID是否和自身相符,如果相符,则表示已经获得该锁,如果不符,则需要测试锁标志位,如果还是偏向锁,就将线程ID设置成当前线程ID,如果不是偏向锁,则需要使用CAS竞争锁(下面会介绍)。

偏向锁的撤销

偏向锁会等到出现其他线程竞争的时候才释放锁,当有其他线程竞争时,会首先暂停拥有该锁的线程,检查该线程是否存活,如果该线程不处于活动状态,则将对象头设置为无锁状态,如果线程仍然活着,要么是锁对象中的线程ID重新偏向于其他线程,要么恢复到无锁状态或将对象标记为不适合偏向锁。

轻量级锁

轻量级锁加锁

JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程尝试使用自旋获得锁。

自旋:线程一般有等待,就绪,运行,阻塞几个状态,线程自旋的意思就是,在线程所需的锁被占用时,线程会不断询问锁是否被释放,保持运行避免进入阻塞状态,这样的话锁一旦被释放,线程能快速响应,提高效率,但同时也造成了一定的资源浪费,适合在锁竞争不激烈的情况下使用。就好比你去拜访一个人,发现他没在家,正常情况下就是先回去,过两天再来拜访,但如果知道主人一会就回来,那在门口等一会儿会更好一点。

轻量级锁解锁

解锁时,会使用原子的CAS操作将Mark Word替换回到对象头,如果成功,则表示没有竞争发生,如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁,以下是两个线程同时争夺锁导致锁膨胀的流程图:

因为自旋会消耗CPU,为了避免无用的自旋,一旦锁级别升级到重量级锁,就不会恢复到原来轻量级锁的状态了,在重量级锁的状态下,凡是其他线程试图获取锁时,都会被阻塞(这样会导致响应时间变慢),当持有锁的线程释放锁时会唤醒这些线程,之后这些线程会进行新一轮的竞争。

总结

猜你喜欢

转载自blog.csdn.net/qq_42734874/article/details/81128490