参考文章
Java中的偏向锁,轻量级锁, 重量级锁解析_萧萧九宸的博客-CSDN博客
本文是本人对以上文章的整理,建议先去看以上文章。
在Java中,一个锁对象的四种状态:
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
在Java中,一个锁就是一个对象
synchronized代码块是由一对monitorenter
和monitorexit
字节码指令实现,这两个指令中间放的就是synchronized中同步代码块中的字节码指令。
在JDK1.6之前,synchronized锁是重量级锁,性能开销严重,从JDK1.6开始,为了改善同步性能,又引入了偏向锁和轻量级锁
锁的重量级的排序是:
无锁
< 偏向锁
< 轻量级锁
< 重量级锁
锁的升级是单向的,只能从低到高,不会出现锁的降级。
随着线程竞争的激烈程度增加,伴随有锁升级的现象,锁的重量程度越来越大。
JVM默认是关闭可偏向锁机制的,如果想要开启可偏向机制,在启动时,通过JVM参数指定
-XX:-UseBiasedLocking
大致的锁膨胀的过程是这样的:
无锁 --> 偏向锁
偏向锁本质上也是一个无锁化编程解决方案。
大部分的情况下,锁不存在多线程竞争,而是总是由同一个线程多次获得,为了减少同一线程获取锁的成本而引入了偏向锁
偏向锁的核心理念就是:如果一个线程获取了锁,那个锁对象就进入偏向模式,“偏向于这个线程”,当这个线程再次请求这个锁时,无需再做同步操作,可以直接获取到该锁,可以省去很多申请锁、加锁的操作,从而减少同步的成本
当初始化锁对象时,会首先判断系统是否开启了“可偏向”机制,如果未开启,则会创建一个普通的无锁状态的对象,并初始化对象头中的内容是hashCode和分代年龄。
若开启了可偏向机制,则会初始化锁对象是可偏向的未偏向状态,即可偏向的无锁状态,此时对象头中MarkWord的内容是 空的线程ID + epoch + 对象分代年龄 + 可偏向标志位
由无锁 到 偏向锁的过程,本质上就是将线程id写入到锁对象的对象头中的MarkWord的过程。
具体的过程是:
- 首先读取目标对象的MarkWord,判断此时锁对象是否处于可偏向状态。
- 如果是可偏向状态,则尝试用CAS操作,将自己的线程ID写入到锁对象的MarkWord中。
- 如果CAS操作成功,则认为此线程已经获取到了对象的偏向锁,执行同步代码块。
- 一个线程执行完之后,并不会将锁对象中的MarkWord中的线程ID赋回空值。这样做的好处是:如果线程需要再次对这个锁对象加锁,而之前此锁对象一直没有被其他线程获取过锁,依旧停留在可偏向的状态下,即可以在不修改对象头的情况下,直接获取该对象的偏向锁,直接认为偏向成功
- 如果CAS操作失败,则说明有另一个线程B抢先获取了偏向锁。这种状态说明此锁的竞争比较激烈,此时需要撤销线程B的偏向锁,将线程B持有的锁升级为轻量级锁。
- 如果是已偏向状态,则检测对象头中的MarkWord中的线程ID是否是当前线程ID
- 如果是,则表明本线程已经获取到了偏向锁,可以直接继续执行同步代码块
- 如果不相等,则证明该对象偏向于其他线程,需要撤销偏向锁。
注意:偏向锁的撤销过程,并不是将锁对象恢复到无锁可偏向的状态,而是直接将该锁对象的状态修改至“轻量级锁定”状态。
偏向锁 --> 轻量级锁
当存在多个线程竞争某一个对象时,会撤销偏向锁,升级到轻量级锁。
当偏向锁加锁失败后,会升级到轻量级锁。
偏向锁在撤销后,锁对象可能处于两种状态:
-
一种是不可偏向的无锁状态 (之所以不可偏向,因为系统已经检测到多于一个线程竞争锁对象,升级到了轻量级锁定状态)
-
另一种是不可偏向的已锁状态 (轻量级锁定)
为什么会出现上述两种状态,因为偏向锁不存在解锁操作,只有撤销操作,触发撤销操作时:
- 原来已经获取了偏向锁的线程可能已经执行完了同步代码块,使得对象处于“闲置状态”,相当于原有的偏向锁已经过期无效了。此时该对象就应该被直接转换为不可偏向的无锁状态
- 原来已经获取了偏向锁的线程可能还没有执行完成同步代码块,偏向锁依旧有效,此时锁对象就应该被转换为轻量级锁定的状态
轻量级加锁的过程
- 在进入同步代码块的时候,如果锁对象的状态是无锁状态,锁标志位是01。
- 会在当前线程的栈帧中创建一个锁记录Lock Record的空间,用来存储锁对象目前的MarkWord的拷贝。(不论是Java方法栈,还是本地方法栈,都是线程私有的)
- 拷贝对象头中的MarkWord到锁记录中。
- 拷贝完成后,JVM会利用CAS操作尝试将锁对象的MarkWord更新为指向锁记录LockRecord的指针,并将Lock Record里的owner指向对象的MarkWord。
- 如果更新成功,那么这个线程就拥有了该对象的锁,并且对象的MarkWord的锁标志位置为“00”,表示此对象处于轻量级锁定的状态。
简单点说上面的过程:
-
首先检查MarkWord中标志位,锁对象是否处于不可偏向的无锁状态
-
然后在当前线程的栈帧中,创建用于存储锁记录LockRecord的空间,并将对象头中的MarkWord拷贝LockRecord中。
-
然后尝试用CAS将对象头中的MarkWord替换为指向锁记录的指针。
- 如果成功,当前线程加锁成功
- 如果失败,表示该对象已经加锁,先进行自旋操作,再次尝试CAS争抢,如果仍未竞争到,进一步升级到重量级锁。
轻量级锁能够提升程序性能的依据是“绝大部分的情况下,在整个同步期间,是不会存在多线程竞争的”,因此轻量级锁的适应场景是线程交替执行同步代码块的场合,如果同一时间访问同一锁的场合,就会膨胀到重量级锁
重量级锁
重量级锁本质就是操作系统内的管程机制,管程机制就是对信号量操作进行了封装。
关于synchronized重量级锁的原理,可以看我的这篇文章关于Java中synchronized的实现原理_秋天code的博客-CSDN博客
锁优化
自旋锁
轻量级锁加锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,会进行自旋锁的优化手段。
如果要在操作系统层面挂起一个线程,会涉及到用户态切换到和心态,这个过程是需要损耗系统性能的。
自旋锁的假设是,当前线程会在不久后可以获取到锁,因此虚拟机会让该线程进行自旋,经过若干轮等待后,如果得到了锁,就进入临界区。如果还得不到锁,那么在操作系统层面就会真的挂起这个线程。
锁消除
锁消除是虚拟机另外一种锁优化的手段,在JIT编译期间,JVM通过对上下文的扫描,发现有一段临界区是不可能存在竞争的,对于这段临界区来说,没有加锁的必要了,因此会消除这个锁。
例如,一个变量是方法内部的,不可能被其他线程访问到,此时就会消除这个方法的同步。