synchronized锁升级机制
文章目录
- 前言
- 传统重量级锁存在的问题
- 锁升级的基本思路
- 偏向锁
- 轻量级锁
- 自旋的优化策略
- 重量级锁
- 锁升级带来的实际价值
前言
在日常开发中,我们经常使用synchronized来保证线程安全,但很多人可能不知道这个关键字在JVM内部经历了怎样的优化过程。早期版本的synchronized性能确实不太理想,但现在已经通过锁升级机制得到了很大改善。
传统重量级锁存在的问题
最初的synchronized实现相当简单粗暴,不管什么情况都直接使用操作系统提供的互斥量,这就意味着每次加锁解锁都要进行系统调用。系统调用需要从用户态切换到内核态,这个过程开销很大。而且一旦锁被占用,其他线程就会被阻塞,需要操作系统来调度唤醒,又是一笔不小的开销。
锁升级的基本思路
JVM发现了这个问题并想出了解决方案:根据实际的竞争情况来选择不同的锁实现。如果没有竞争或竞争很少,就用轻量级的方式;如果竞争激烈,再升级到重量级锁。
这个升级过程是这样的:无锁状态 → 偏向锁 → 轻量级锁 → 重量级锁。
每个Java对象都有一个对象头,里面有个叫Mark Word的区域专门用来存储锁的信息。
偏向锁
偏向锁的设计思路很直接:既然大部分情况下同步块都只有一个线程在访问,那就干脆把锁"偏向"这个线程好了。
当一个线程第一次获得锁时,JVM就把这个线程的ID记录在对象头里。以后这个线程再进入同步块时,只需要检查一下对象头里的线程ID是不是自己就行了,连CAS操作都不需要,这样性能就很好了。
public class Example {private final Object lock = new Object();public void doWork() {synchronized (lock) {// 如果只有一个线程反复调用这个方法// 第一次会设置偏向锁,后面进入几乎没有开销}}
}
但是偏向锁也有失效的时候。如果来了其他线程想要获取锁,偏向锁就得撤销了,这个过程还是有一定开销的。
轻量级锁
当偏向锁撤销后,或者有多个线程轮流访问同步块时,就该轻量级锁上场了。轻量级锁的核心思想是用CAS操作来替代重量级的互斥量。
轻量级锁的工作过程是这样的:线程要获取锁时,先在自己的栈帧中建立一个叫锁记录的空间,然后用CAS操作尝试把对象头的内容复制到这个锁记录中,同时把对象头更新成指向锁记录的指针。如果CAS成功了,就说明获得了锁;如果失败了,可能是其他线程已经获得了锁。
public class Example {private final Object lock = new Object();public void method1() {synchronized (lock) {// 线程A执行}}public void method2() {synchronized (lock) {// 线程B稍后执行,两个线程错开时间访问// 这种情况适合使用轻量级锁}}
}
自旋的优化策略
如果CAS失败了,线程并不会马上阻塞,而是会自旋一段时间,也就是在那里空转等待,看看锁会不会很快被释放。这是基于一个假设:大部分同步块的执行时间都很短,与其阻塞线程再唤醒,不如让线程等一等,因为如果阻塞时间很短,那么就让当前线程自旋,这是自旋性能代价小;但是如果阻塞时间很长,那不要一直自旋,直接阻塞线程就好了。
不过自旋也不能无限进行下去,如果自旋了一定次数还没有获得锁,就说明竞争比较激烈,这时候就会升级到重量级锁。
重量级锁
当自旋也解决不了问题,或者有很多线程同时竞争锁时,就只能使用重量级锁了。这时候JVM会使用操作系统提供的互斥量,没有获得锁的线程会被阻塞,需要等待操作系统来调度唤醒。
重量级锁虽然开销大,但是在高竞争的场景下反而是最合适的选择。因为如果竞争很激烈,让线程自旋反而会浪费CPU资源,还不如直接阻塞,等有机会再唤醒。
public class Example {private final Object lock = new Object();public void highContentionMethod() {synchronized (lock) {// 如果有很多线程同时竞争这个锁// 最终会升级为重量级锁try {Thread.sleep(100); // 模拟长时间操作} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}
}
锁升级带来的实际价值
锁升级机制的最大好处就是能够根据实际情况选择最合适的锁实现。在没有竞争或竞争很少的情况下,偏向锁和轻量级锁的性能要比重量级锁好很多。据统计,在很多应用中,大部分synchronized代码块都没有多线程竞争,这种情况下锁升级能够显著提高性能。