Java 锁相关详解【五、Java 并发中的自旋锁与无锁编程】
Java 并发中的自旋锁与无锁编程
在前几篇文章中,我们讨论了 synchronized、ReentrantLock、ReadWriteLock、StampedLock 等锁机制。它们的共同点是 线程在竞争不到锁时,会被阻塞,等待被唤醒。
但是阻塞/唤醒操作涉及 用户态与内核态切换,代价非常昂贵。
为了减少这种上下文切换的开销,JVM 和 JUC 引入了 自旋锁(Spin Lock) 与 无锁编程(Lock-Free Programming) 技术。
它们通过 CPU 忙等待(busy-wait) 或 CAS(Compare-And-Swap)原子操作,在高并发场景下能大幅提升性能。
一、自旋锁(Spin Lock)
1.1 基本原理
- 普通锁:线程获取不到锁 → 进入阻塞状态 → 操作系统调度唤醒。
- 自旋锁:线程获取不到锁 → 不立即阻塞,而是在 用户态循环检查锁是否可用,若短时间后可用则直接获取。
这种方式避免了线程切换的开销,但消耗 CPU。
1.2 JVM 中的自旋锁
在 HotSpot JVM 中,偏向锁 与 轻量级锁 都使用了自旋机制:
- 当一个线程尝试获取轻量级锁失败时,它会先进行若干次自旋尝试。
- 如果在自旋期间锁释放了,就直接获取成功;
- 如果自旋超过一定次数(默认 10 次,可通过
-XX:PreBlockSpin调整),仍未成功,就会升级为重量级锁并阻塞。
这种 自适应自旋(Adaptive Spinning) 可以动态调整自旋次数,提升性能。
1.3 简单示例
class SpinLock {
private final AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
while (!owner.compareAndSet(null, current)) {
// 自旋等待
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
这里使用了 AtomicReference + CAS 来实现自旋锁。
1.4 优缺点
- 优点:避免阻塞,适合锁持有时间非常短的场景。
- 缺点:CPU 空转浪费资源;在多核高并发下可能导致性能下降。
二、无锁编程(Lock-Free Programming)
2.1 背景
锁机制的核心问题是:
- 会导致 线程阻塞与上下文切换;
- 存在 死锁、优先级反转 等风险。
为了解决这些问题,现代并发编程广泛使用 CAS 原子操作,通过 乐观并发控制 来避免加锁。
2.2 CAS(Compare-And-Swap)
CAS 是 CPU 提供的原子指令,形式如下:
boolean CAS(address, expectedValue, newValue)
含义:
- 如果内存地址
address的值等于expectedValue,则更新为newValue,返回 true; - 否则不更新,返回 false。
Java 中通过 Unsafe 和 VarHandle 提供 CAS 操作,JUC 的 Atomic 系列类就是基于 CAS 实现的。
2.3 常见的无锁数据结构
-
原子计数器
AtomicInteger counter = new AtomicInteger(0); counter.incrementAndGet(); // CAS 实现 -
无锁栈(Treiber Stack)
class LockFreeStack<T> { private final AtomicReference<Node<T>> head = new AtomicReference<>(); public void push(T value) { Node<T> newNode = new Node<>(value); Node<T> oldHead; do { oldHead = head.get(); newNode.next = oldHead; } while (!head.compareAndSet(oldHead, newNode)); } public T pop() { Node<T> oldHead; Node<T> newHead; do { oldHead = head.get(); if (oldHead == null) return null; newHead = oldHead.next; } while (!head.compareAndSet(oldHead, newHead)); return oldHead.value; } static class Node<T> { final T value; Node<T> next; Node(T value) { this.value = value; } } } -
无锁队列(Michael-Scott Queue):JDK 中的
ConcurrentLinkedQueue实现。
三、自旋锁 vs 无锁编程
| 特性 | 自旋锁 | 无锁编程 (CAS) |
|---|---|---|
| 阻塞/非阻塞 | 可能阻塞(最终可能进入重量级锁) | 完全非阻塞 |
| 实现方式 | 自旋等待 + CAS | CAS 原子操作 |
| 性能特点 | 锁持有时间短时高效 | 乐观并发,高度扩展性 |
| 风险 | CPU 空转浪费 | ABA 问题(需配合 AtomicStampedReference) |
| 使用场景 | 轻量级锁、锁竞争低 | 高并发下的队列、计数器、缓存系统 |
四、工程实践与优化
-
自旋锁
- 仅在锁持有时间极短时使用。
- JVM 默认的偏向锁与轻量级锁已内置自旋机制,应用层极少需要手动实现。
-
无锁编程
- 尽量使用 JUC 提供的原子类和并发容器(如
AtomicInteger、ConcurrentLinkedQueue)。 - 避免自己实现复杂的 lock-free 数据结构,除非对性能有极致要求。
- 尽量使用 JUC 提供的原子类和并发容器(如
-
ABA 问题
- CAS 可能出现 ABA 问题(值从 A 变为 B 再变回 A,CAS 不可察觉)。
- 解决方法:使用 版本号 或
AtomicStampedReference。
-
性能评估
- 在低并发下,自旋锁/无锁未必优于传统锁。
- 在高并发、低延迟场景下,CAS 更加合适。
五、总结
- 自旋锁:利用忙等待减少线程切换,适合短期锁竞争。
- 无锁编程:基于 CAS 原子操作,提供非阻塞的并发控制,更具可扩展性。
- JVM 内置锁优化:偏向锁、轻量级锁本质上都使用了自旋与 CAS 技术。
- 工程实践:优先使用 JUC 提供的高性能并发类,而非自行造轮子。
一句话总结:
自旋锁与无锁编程是现代 Java 并发性能优化的核心基石,它们让我们能够在极端高并发下,避免传统锁的上下文切换开销,实现近乎线性扩展的性能表现。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120376

浙公网安备 33010602011771号