Synchronized实现同步的方式有三种:偏向锁、轻量级锁、重量级锁。本文会从理论和代码实践两方面阐述三种锁的实现细节和原理。
偏向锁的思想很简单,就是偏向于第一个获取锁的线程,当其他线程要获取锁时,会在CAS操作中失败,然后挂起等待,直到第一个线程释放锁。这个锁的好处是可以满足大多数同步场景下的需求,并且消耗很小的资源。
要开启偏向锁,需要添加JVM参数-XX:+UseBiasedLocking
。
public class BiasedLocking {
private Object lock = new Object();
public void method1() {
synchronized (lock) {
// do something...
}
}
}
当第一个线程进入synchronized
块时,会将锁的标记从none
修改为bias
状态,同时记录偏向的线程ID。之后其他线程要获取锁,会通过CAS操作尝试将锁偏向自己,但这个操作会失败,所以只会短暂地竞争,很快其他线程就会进入阻塞状态,释放CPU时间片。
当偏向的线程退出同步块时,如果发现锁还没有其他线程在等待,那么会将锁的状态重置为none
。如果发现有其他线程在等待,会释放锁,让等待线程获取。
当偏向的线程退出同步块时,如果发现锁还没有其他线程在等待,那么会将锁的状态重置为none
。如果发现有其他线程在等待,会释放锁,让等待线程获取。
轻量级锁的获取过程是通过CAS操作完成的。当前线程会先在对象头中记录自己,然后尝试用CAS将对象头中的锁记录替换为当前线程,如果成功就获取到锁,失败就进入阻塞队列等待唤醒。
public class LightWeightLock {
private Object lock = new Object();
public void method1() {
synchronized (lock) {
// do something...
}
}
}
当第一个线程进入方法时,会将对象头中的锁状态修改为当前线程ID,然后进入同步块。其他线程要获取锁时,会先检查对象头的锁状态,如果发现锁已经被占用,那么会使用CAS操作进行抢占,如果成功则获取到锁,失败会加入到阻塞队列进行等待。
当前线程退出同步块时,会使用CAS操作释放锁,将对象头设置为unlocked
状态,同时唤醒阻塞队列中的一个等待线程。
轻量级锁的优点是消耗资源小,对代码性能的影响小,但是在高并发的场景下,CAS操作的 ABA问题会导致线程无法正常工作,所以当锁重入超过10次,或者锁持有时间超过1s时,JVM会将轻量级锁升级为重量级锁。
重量级锁会导致当前拥有锁的线程和其他等待线程都进入阻塞状态,切换到内核态,这 obviously 是一个非常消耗资源的操作。
其实现过程是:当前线程首先会在对象头中记录自己,然后进入内核态被阻塞,同时其他线程也会被阻塞。当其中一个线程退出同步块时,会唤醒其他线程中的一个
public class HeavyWeightLock {
private Object lock = new Object();
public void method1() {
synchronized (lock) {
// do something...
}
}
}
当第一个线程进入同步块时,会标记对象头表示此对象处于锁定状态,然后进入内核态挂起。其他线程要获取锁时,会发现对象头的锁定状态,也会进入内核态挂起。
当锁定的线程退出同步块时,会标记对象头为解锁状态,然后唤醒一个等待线程。被唤醒的线程会重新标记对象头为锁定状态,然后继续执行同步块中的内容。
重量级锁的优点是可以解决轻量级锁中的ABA问题,但是其性能消耗也是最大的。所以如果一个锁仅被一个线程使用,或有很高的重入概率,那么应选择偏向锁或轻量级锁,可以获得更高的性能。
偏向锁适用于单线程环境,性能最高;轻量级锁通过CAS实现,性能较好,但是会出现ABA问题;
要实际观察Synchronized锁的三种状态转换,可以使用JDK自带的JMC(Java Mission Control)工具。
下面是具体的操作步骤:
通过上述步骤,我们可以直观的观察Synchronized锁的三种状态之间的切换过程,这也是理解其原理的最佳途径。
在实际项目中,我们如何根据场景选择和设置合适的锁机制呢?这里提供一些参考建议:
-XX:-UseBiasedLocking
关闭。-XX:+EliminateLocks
开启,JVM会分析代码,消除不可能存在竞争的锁,以提高性能。-XX:+DoEscapeAnalysis
开启,JVM会分析代码,将多个连续的加锁操作锁合并为一个,以减少加锁操作次数。-XX:PreBlockSpin
设置自旋次数,指令在获取锁失败先自旋一定次数,再进入阻塞状态,以减少线程切换。但是如果自旋太长,会消耗CPU,需要根据场景设置。-XX:MonitorTimeout=x
设置重量级锁定超时时间,以避免线程因锁定过长出现死锁现象。除上述JVM层面的设置外,在代码层面我们也可以根据场景选择不同的锁来提高性能,比如使用ReentrantLock代替Synchronized等。
理解Synchronized锁及其实现原理,是Java后端工程师的基础知识之一,这部分内容在面试中也常会涉及,下面是一些可能的面试题:
锁升级的目的是为了提高并发性能。偏向锁适用于单线程,升级为轻量级锁可以适应更高的并发;轻量级锁使用CAS有性能损耗,升级为重量级锁可以解决该问题。
两者的主要区别如下: