一、前言
Java 1.6 时引入了“偏向锁” 和 “轻量级锁”,级别从低到高依次是 :无锁,偏向锁,轻量级锁,重量级锁。这些状态会随着竞争而升级。下面我们就实操来研究一下升级过程,不过需要一些前提知识
二、对象内存布局
我们都知道对象在堆里存放的,那么它的内部结构是怎样的呢,下面以64为操作系统来说明
首先对象包含对象头,实例数据,对齐填充。
对象头:包含mark word 和 Klass 指针 ,在64位下,mark word 占 8个字节,Klass占8个字节如果对象是数组类型还会有一块标志数组长度的数据是4个字节
实例数据:对象中包含的实例变量,不包括静态变量,静态变量不属于对象
对齐填充:对象的大小都必须是8的整数倍,如果前两者不是8的整数倍就会在这里填充字节,使对象达到8的整数倍
三、具体流程
synchronized用的锁是存在Java对象头里的Mark Word中,锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位
- 偏向锁:MarkWord存储的是偏向的线程ID;
- 轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针;
- 重量锁:MarkWord存储的是指向堆中的monitor对象的指针;
- 将锁信息放入对象头中
- 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
- 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
- 实现轻量级锁的时候大概率用到的自旋锁策略
- 是一种不公平锁
- 是一种可重入锁
- 不是读写锁
引入对象头后synchronized的重量级锁流程
四、阶段介绍
1、轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻寨
主要作用
- 有线程来参与锁的竞争,但是获取锁的冲突时间极短
- 本质就是自选锁CAS
- 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
2、偏向锁
当线程A第一次竞争到锁时,通过修改Mark Word中的偏向ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
主要作用
当一段同步代码一直被同一个线程多次访问,由于只有一个线程访问那么该线程在后续访问时便会自动获得锁。
- 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
- Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
- 如果是轻量级锁,就算是同一个线程,也会生成对应的Lock Record,和对应的对象的Mard Word进行CAS操作,只是CAS不成功;CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用重量级锁(Monitor->Mutex Lock), 这里的排他时间要短的多
- 只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
五、总结
Synchronized锁的升级过程主要包括以下几个阶段:
- 无锁状态:初始状态下,对象没有被任何线程锁定,所有的线程都可以尝试获取锁。
- 偏向锁状态:当线程第一次获取锁时,对象头中的标记位会变为偏向锁状态,同时记录获取锁的线程ID。此后,如果该线程再次进入同步块,JVM会检查对象头的Mark Word是否指向当前线程的线程ID,如果是,则无需使用CAS操作加锁,直接进入同步块,这就是偏向锁的支持重入的特性。这个过程中并不会涉及到锁的升级。
- 轻量级锁状态:当另一个线程尝试获取这个已经被偏向的锁时,偏向锁就会升级为轻量级锁。此时,JVM会通过自旋锁的方式尝试获取锁,自旋就是不会立即阻塞线程,而是让线程空转等待锁释放。如果自旋成功则获取锁,执行同步块;如果自旋失败,则锁升级为重量级锁。
- 重量级锁状态:在这个状态下,未抢到锁的线程都会被阻塞,等待操作系统唤醒。此时的性能开销最大,因为线程的阻塞和唤醒都需要操作系统来协助完成。
Synchronized锁的升级过程是为了在保证线程安全的前提下,尽可能地提高程序的性能。通过锁的升级,可以实现在线程竞争不激烈的情况下使用效率更高的锁,而在线程竞争激烈的情况下使用更稳定的锁,从而在各种场景下都能保持较好的性能。