Synchronized锁升级机制

110 阅读4分钟

为了减少获得锁释放锁带来的性能消耗,Java引入了偏向锁和轻量级锁。算上无锁和重量级锁,锁一共有四种状态。级别从高到低依次是:无锁状态、偏向锁状态、轻量级锁状态、和重量级锁状态、这几个锁状态会随着竞争情况逐渐升级。锁可以升级却不能降级,这种策略是为了提高获得锁和释放锁的效率。下面我将逐一介绍这几种锁状态是如何加锁以及释放锁的。

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁的加锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID(没有加锁),以后线程再进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要测试一下对象头中的Markword里是否储存着指向当前线程的偏向锁。如果测试成功,则表明该线程已经获得了锁。如果测试失败则需要测试一下MarkWord中偏向锁的标识是否设置成1(如果是1表示锁状态为偏向锁)。如果没设置则使用CAS竞争锁。如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的解锁

偏向锁使用了一种等到竞争才释放锁的机制。

也就是说即便线程执行结束了,也不会释放偏向锁(好吧它其实也没加锁)当其他线程尝试竞争锁的时候,持有偏向锁的线程才会释放锁(也就是其他线程通过CAS将作为锁的对象的对象头中的MarkWord中存储的线程ID更改为自己线程ID的时候)。

偏向锁的撤销需要等在全局安全点。他首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着。如果线程不处于活动状态,则将对象头设置成无锁状态。如果还活着要么将持有锁的线程置为无锁状态,要么将锁偏向其他线程。

图解如下。依旧是《java并发编程的艺术》中的原图。

image.png 大家注意当线程2使用CAS竞争锁失败的时候,说明当前锁竞争比较激烈,会将锁升级到轻量级锁。也可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

轻量级锁

轻量级锁加锁

当线程获取偏向锁失败,就会尝试自旋获取锁。(类似一个while循环不断尝试获取锁),这时候就相当于加上了轻量级锁。

轻量级锁解锁

前面说到,持有偏向锁的线程会在该线程的栈帧中存储锁对象的MarkWord,同时锁对象的MarkWord内容也会被替换成持有锁的线程的线程ID,双方互换人质。升级到轻量级锁后其实也差不多,只不过MarkWord中的数据从线程ID变成了指向栈中锁记录的指针。可以说就是因为要跟锁对象互换人质的线程多了,才加的轻量级锁。那么轻量级锁解锁,自然就是把人质还回去。这个过程也是通过CAS来完成。

如果说交换人质的时候发现,嘿你MarkWord里面存的不是指向我线程栈中的锁记录的指针(此时有别的线程通过CAS将锁对象MarkWord中的内容换成了自己的线程栈中锁记录的指针),你在外面有别的线程了!锁升级!换重量级锁,锁死。直到该线程操作完成,竞争锁的线程才会从等待状态中被唤醒。

如果CAS解锁失败说明此时并发量已经很大,而CAS自旋是一个类似while的操作,在不断循环十分占用计算机资源。所以一旦升级到重量级锁就不会再降级。

图解如下

image.png

最后的结尾再给大家看一下上一张介绍MarkWord的图(上一篇文章主要介绍重量级锁,本文不再赘述)

image.png 经过本文的讲解是不是就清楚为什么随着锁的升级MarkWord的内容会发生变化了?