偏向锁
偏向锁是一种Java虚拟机(JVM)在多线程环境下优化同步性能的锁机制,它适用于大多数时间只有一个线程访问同步代码块的场景。当一个线程访问同步代码块时,JVM会把锁偏向于这个线程,后续该线程在进入和退出同步代码块时,无需再做任何同步操作,从而大大降低了获取锁和释放锁的开销。偏向锁是Java内存模型中锁的三种状态之一,位于轻量级锁和重量级锁之前
示例场景和示例代码
- 适用场景:单线程重复操作
- 示例场景:系统启动时的配置加载
- 说明:系统启动时加载全局配置(如数据库连接参数),后续仅监控线程定时检查配置(无竞争)。
public class ConfigLoader {
private static final Object configLock = new Object();
private static Map<String, String> config;
// 系统启动时初始化配置(单线程操作)
public static void init() {
synchronized (configLock) {
config = loadConfigFromDB(); // 耗时操作
}
}
// 监控线程定期读取配置(无竞争)
public static void refresh() {
synchronized (configLock) { // 偏向锁生效
if (isConfigModified()) {
config = reloadConfig();
}
}
}
}
锁行为:
init()
首次触发偏向锁(偏向初始化线程)。后续
refresh()
由同一线程执行,无同步开销。
偏向锁的优缺点:
- 优点:
- 对于没有或很少发生锁竞争的场景,偏向锁可以显著减少锁的获取和释放所带来的性能损耗。
- 缺点:
- 额外存储空间:偏向锁会在对象头中存储一个偏向线程ID等相关信息,这部分额外的空间开销虽然较小,但在大规模并发场景下,累积起来也可能成为可观的成本。
- 锁升级开销:当一个偏向锁的对象被其他线程访问时,需要进行撤销(revoke)操作,将偏向锁升级为轻量级锁,甚至在更高竞争情况下升级为重量级锁。这个升级过程涉及到CAS操作以及可能的线程挂起和唤醒,会带来一定的性能开销。
- 适用场景有限:偏向锁最适合于绝大部分时间只有一个线程访问对象的场景,这样的情况下,偏向锁的开销可以降到最低,有利于提高程序性能。但如果并发程度较高,或者线程切换频繁,偏向锁就可能不如轻量级锁或重量级锁高效。
轻量锁
轻量级锁是一种在Java虚拟机(JVM)中实现的同步机制,主要用于提高多线程环境下锁的性能。它不像传统的重量级锁那样,每次获取或释放锁都需要操作系统级别的互斥操作,而是尽量在用户态完成锁的获取与释放,避免了频繁的线程阻塞和唤醒带来的开销。轻量级锁的作用主要是减少线程上下文切换的开销,通过自旋(spin-wait)的方式让线程在一段时间内等待锁的释放,而不是立即挂起线程,这样在锁竞争不是很激烈的情况下,能够快速获得锁,提高程序的响应速度和并发性能
在Java中,轻量级锁主要作为JVM锁状态的一种,它介于偏向锁和重量级锁之间。当JVM发现偏向锁不再适用(即锁的竞争不再局限于单个线程)时,会将锁升级为轻量级锁。
轻量级锁适用于同步代码块执行速度快、线程持有锁的时间较短且锁竞争不激烈的场景,如短期内只有一个或少数几个线程竞争同一线程资源的情况
在Java中,轻量级锁的具体实现体现在 java.util.concurrent.locks 包中的 Lock 接口的一个具体实现:java.util.concurrent.locks.ReentrantLock,它支持可配置为公平或非公平模式的轻量级锁机制,当使用默认构造函数时,默认是非公平锁(类似于轻量级锁的非公平性质)。不过,JVM的内置 synchronized 关键字在JDK 1.6之后引入了锁升级机制,也包含了偏向锁和轻量级锁的优化。
示例场景和示例代码
- 适用场景:低竞争短任务
- 案例:多线程更新独立计数器
- 说明:每个线程更新专属计数器(如用户请求计数),偶尔发生锁竞争。
public class LightweightLockExample {
private static class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) { // 轻量级锁
count++; // 快速操作
}
}
}
public static void main(String[] args) {
Counter[] counters = new Counter[4];
for (int i = 0; i < counters.length; i++) {
counters[i] = new Counter();
}
// 模拟低竞争:多个线程操作不同计数器
for (int i = 0; i < 100; i++) {
int idx = i % counters.length;
new Thread(() -> counters[idx].increment()).start();
}
}
}
锁行为:
线程操作独立计数器时,无竞争(轻量级锁直接生效)。
偶尔哈希冲突导致竞争时,通过自旋快速获取锁
轻量锁的优缺点:
- 优点:
- 低开销:轻量级锁通过CAS操作尝试获取锁,避免了重量级锁中涉及的线程挂起和恢复等高昂开销。
- 快速响应:在无锁竞争或者锁竞争不激烈的情况下,轻量级锁使得线程可以迅速获取锁并执行同步代码块。
- 缺点:
- 自旋消耗:当锁竞争激烈时,线程可能会长时间自旋等待锁,这会消耗CPU资源,导致性能下降。
- 升级开销:如果自旋等待超过一定阈值或者锁竞争加剧,轻量级锁会升级为重量级锁,这个升级过程本身也有一定的开销。
重量锁
重量级锁是指在多线程编程中,为了保护共享资源而采取的一种较为传统的互斥同步机制,通常涉及到操作系统的互斥量(Mutex)或者监视器锁(Monitor)。在Java中,通过synchronized关键字实现的锁机制在默认情况下就是重量级锁。确保任何时刻只有一个线程能够访问被锁定的资源或代码块,防止数据竞争和不一致。保证了线程间的协同工作,确保在并发环境下执行的线程按照预定的顺序或条件进行操作。
在Java中,重量级锁主要指的是由 synchronized 关键字实现的锁,它在JVM内部由Monitor实现,属于内建的锁机制。另外,java.util.concurrent.locks 包下的 ReentrantLock 等类也可实现重量级锁,这些锁可以根据需要调整为公平锁或非公平锁。
示例场景和示例代码
- 适用场景:高竞争长任务
- 案例:订单支付系统
- 说明:多线程同时处理支付订单(竞争激烈),且支付流程包含网络IO(长耗时操作)。
public class PaymentSystem {
private final Object paymentLock = new Object();
public void processPayment(Order order) {
synchronized (paymentLock) { // 重量级锁
validateOrder(order); // 校验订单
deductInventory(order); // 扣减库存(耗时)
callPaymentGateway(order); // 调用支付接口(网络IO)
updateOrderStatus(order); // 更新状态
}
}
// 模拟高并发支付
public static void main(String[] args) {
PaymentSystem system = new PaymentSystem();
for (int i = 0; i < 100; i++) {
new Thread(() -> system.processPayment(new Order())).start();
}
}
}
锁行为:
多线程激烈竞争锁时,升级为重量级锁。
未抢到锁的线程被操作系统挂起(避免CPU空转)。
重量级锁的优缺点
- 优点:
- 强一致性:重量级锁提供了最强的线程安全性,确保在多线程环境下数据的完整性和一致性。
- 简单易用:synchronized关键字的使用简洁明了,不易出错。
- 缺点:
- 性能开销大:获取和释放重量级锁时需要操作系统介入,可能涉及线程的挂起和唤醒,造成上下文切换,这对于频繁锁竞争的场景来说性能代价较高。
- 延迟较高:线程获取不到锁时会被阻塞,导致等待时间增加,进而影响系统响应速度。
三种锁的对比总结:
场景 | 锁类型 | 触发条件 | 性能影响 |
单线程重复操作 | 偏向锁 | 同一线程多次访问 | 无同步开销 |
低竞争短任务 | 轻量级锁 | 多线程交替竞争(自旋成功) | 少量CPU自旋消耗 |
高竞争或长任务 | 重量级锁 | 自旋失败/竞争激烈 | 线程挂起唤醒开销大 |
注:
-
从JDK 15开始,偏向锁默认禁用(可通过
-XX:+UseBiasedLocking
手动启用)。 -
轻量级锁在竞争加剧时会升级为重量级锁(JVM自动管理)。
-
长任务建议用
ReentrantLock
替代synchronized
(支持超时和公平锁)。