一.Java对象头
Synchronized用的锁是存储在Java对象头的,所以理解Java对象头的存储结构和存储数据的类型有助于对锁的理解;
Java对象头中主要存储三类数据:
- 第一类叫做MarkWord,主要存储对象的hashcode,分代年龄,锁信息等运行数据;
- 第二类是Class Pointer,指向方法区中该class的对象,JVM通过此字段来判断当前对象是哪个类的实例;
- 第三类,数组的长度,就是如果当前对象是数组的话才会有。
三类中,我们这里重点关注第一类MarkWord,是我们理解锁的核心
|------|-------------|--------|------------|----------|
| 锁状态 | 25bit | 4bit | 1bit是否为偏向锁 | 2bit锁标志位 |
| 无锁状态 | 对象的hashcode | 对象分代年龄 | 0 | 01 |
程序运行期间,MarkWord存储的信息会随着锁标志位的变化而变化,可能会变化为以下四种状态之一;
| 锁状态 | 25bit | 4bit | 偏向锁标志(1bit) | 锁标志位(2bit) |
|---|---|---|---|---|
| 偏向锁 | 23位:线程ID 2位:Epoch | ✔ | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
| 重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | ||
| GC标记 | 空 | 11 |
会发现无锁、偏向锁的"锁标志位"是一样的,即都是01,这是因为无锁、偏向锁是靠字段"是否是偏向锁"来区分的,0代表没有启用偏向锁,1代表启用偏向锁
二.锁的升级与对比
那么我们现在知道了,锁有四种状态,分别是:无锁,偏向锁,轻量级锁,重量级锁。这几种锁的状态会随着并发竞争的情况逐渐升级,锁只能升级不能降级(也就是说轻量级锁不能变成偏向锁)。
2.1 偏向锁
在大多实际环境下,锁不仅不存在多线程竟争,而且总是由同一个线程多次获取,那么在同一个线程反复获取所释放锁中,其中并没有锁的竟争,那么这样看上去,多次的获取锁和释放锁带来了很多不必要的性能开销和上下文切换。所以引入了偏向锁来处理这种情况。
通过你会发现无锁、偏向锁的"锁标志位"是一样的,即都是01,这是因为无锁、偏向锁是靠字段"是否是偏向锁"来区分的,0代表没有启用偏向锁,1代表启用偏向锁,可以通过JVM参数(XX:UseBiasedLocking=true 默认)控制。并且启动偏向锁还有延迟(默认4秒),可以通过JVM参数(XX:BiasedLockingStartupDelay=0)来关闭延迟.
**偏向锁加锁:**当一个线程A访问同步块并获取锁时,会在对象头存储锁偏向的线程ID,以后该线程在进入和退出 同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的MarkWord里是否存储着指向当前线程A的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试-下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁):如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。如果没有设置,则使用CAS竞争锁(即轻量级锁)

例子:当大呆需要上WC时,只有它自已要上WC,此时并没有其它的人需要上。WC,那么这时这个WC可以直接给大呆使用,并且大呆把可以标识自已身份的ID贴到门上,表示此时大呆占用了这个WC。
偏向锁撤销:还是用上面这个图来解释,此时当前的WC被大呆所占用,这时二呆来了也要使用WC。这时先等大呆解决完(执行完这次任务),大呆和二呆就要通过CAS的方式来抢占WC。因为此时锁的状态是偏向锁的状态,二呆来了也要使用WC(这时有两个人同时要使用WC,这时就要将偏向锁升级成轻量级锁),在升级轻量锁之前首先需要将WC上的标识大呆身份的ID撕下来(这一步叫做偏向锁的撤销)。
2.2 轻量级锁
上面锁被撤销后,升级为了轻量级锁,轻量级锁状态下两个人需要通过过自旋+CAS的方式两个人来抢锁。当其中一个线程抢锁成功后,会将LR贴到WC的门上,表示WC当前被某个线程占用,然后另一个没有抢到锁的线程就一直自旋获取锁。
- 自旋的意思是占用CPU来反复尝试获取锁,直到获取成功
- LR是Lock Record锁记录
LR的锁记录中存储的是对象的MarkWord的备份,即拷贝进入的,而++两个线程竞争的过程就是通过CAS的方式将对象本来的MarkWord位置存储的信息替换为指向自己LR记录的指针。谁替换成功了,谁就获得了锁++ ,例如A成功了。那没有获取到锁的线程B,就再自旋一段时间(自旋的原因是因为B认为A很快就能执行完,我就在门口等一下,也就是B认为竞争没有那么激烈)。当自旋-段时间后,如果还没有获得锁,那B就只能将锁修改为重量级锁了,然后所有竞争锁的线程进入阻塞状态,等待A执行完之后唤醒。

2.3 重量级锁
重量级锁,线程加锁失败会进入阻塞状态,等待前驱获得线程的锁执行完之后唤醒。
总结:
| 锁类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
| 轻量级锁 | 竞争的线程不会阻塞,提高程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 |
| 重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
注意,偏向锁和重量级锁并没有使用monitor