深入浅出Java自旋锁:从原理剖析到高并发实战案例
目录导读
- 什么是自旋锁?—— 从“阻塞”到“忙等”的思维转变
- 自旋锁的底层原理:CAS与CPU指令
- Java中的自旋锁实现:从
AtomicInteger到AbstractQueuedSynchronizer - 实战案例:使用自旋锁实现一个高性能计数器
- 自旋锁的致命缺陷:ABA问题与公平性缺失
- 自适应自旋与
LockSupport:JVM的终极优化 - 高频面试问答:自旋锁 vs 阻塞锁,你怎么选?
什么是自旋锁?—— 从“阻塞”到“忙等”的思维转变
在并发编程中,当线程A持有锁时,线程B如果尝试获取同一把锁,通常有两种策略:

- 阻塞锁:线程B立即挂起(
BLOCKED状态),进入操作系统内核态,等待线程A释放后唤醒,开销巨大(涉及上下文切换)。 - 自旋锁:线程B不放弃CPU,而是通过循环“忙等”(
spin),不断检查锁是否释放,如果锁持有时间很短,自旋等待比阻塞挂起性能高出一个数量级。
核心思想:用CPU时间片换取线程切换的开销。
自旋锁的底层原理:CAS与CPU指令
自旋锁的基石是CAS(Compare-And-Swap,比较并交换),在Java中,Unsafe类提供了compareAndSwapInt方法,最终调用CPU的cmpxchg指令。
伪代码实现一个自旋锁:
public class SpinLock {
private final AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
// 自旋:如果当前持有者不为空,则一直循环
while (!owner.compareAndSet(null, current)) {
// CAS失败说明锁被占用,空转等待
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
关键点:owner.compareAndSet(null, current)是一个原子操作,同一时刻只有一个线程能成功将null改为自己,视为获取锁。
Java中的自旋锁实现:从AtomicInteger到AbstractQueuedSynchronizer
AtomicInteger:利用getAndIncrement的CAS循环,本质是“非阻塞自旋”。java.util.concurrent.atomic包:所有原子类都依赖自旋实现。AbstractQueuedSynchronizer(AQS):复杂得多,AQS内部使用了CLH队列锁,但前驱节点释放锁时,后继节点会通过LockSupport.park阻塞,但如果短临界区,JVM会启用自适应自旋(默认开启,阈值基于前一次旋转次数)。
实战案例:使用自旋锁实现一个高性能计数器
假设有一个秒杀系统的库存扣减需求,要求并发量高且不允许超卖,使用AtomicInteger模拟自旋:
public class SpinCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
while (true) {
int oldValue = count.get();
int newValue = oldValue + 1;
// 关键:CAS失败则重试,这就是自旋
if (count.compareAndSet(oldValue, newValue)) {
break;
}
}
}
public int getCount() {
return count.get();
}
public static void main(String[] args) throws InterruptedException {
SpinCounter counter = new SpinCounter();
// 模拟100个线程并发累加
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
}).start();
}
Thread.sleep(1000);
System.out.println("最终结果: " + counter.getCount()); // 必定输出100000
}
}
运行结果分析:没有使用synchronized,也没有加锁,但保证了原子性和可见性(AtomicInteger内部有volatile变量)。
自旋锁的致命缺陷:ABA问题与公平性缺失
- ABA问题:线程A读取值为A,此时线程B改为B又改回A,线程A的CAS会误以为值未变,解决方案:使用
AtomicStampedReference(带版本号)。 - 公平性:自旋锁是不公平的,后来线程可能“插队”,导致先来的线程饿死。
- CPU浪费:如果锁持有时间过长(比如IO操作),自旋会白白消耗CPU。
自适应自旋与LockSupport:JVM的终极优化
JVM对自旋进行了升级:
- 自适应自旋:如果前一次自旋成功(锁很快释放),则下次自旋次数增加;反之则减少甚至挂起。
LockSupport.park:AQS在自旋失败后,会调用park()将线程挂起,避免CPU空转,这实际上是“先自旋,再阻塞”的混合策略。
高频面试问答:自旋锁 vs 阻塞锁,你怎么选?
Q1:什么场景使用自旋锁? 答:临界区执行时间极短(比如简单的计数、状态标记),且并发竞争不激烈,此时线程切换开销大于自旋开销。
Q2:自旋锁一定比阻塞锁快吗?
答:不一定,如果临界区有IO、锁竞争激烈,自旋会导致CPU飙高,吞吐量断崖式下跌,此时用synchronized(JDK 6后优化为重量级锁)更合适。
Q3:synchronized内部使用自旋吗?
答:是的,在JDK 6之后,监视器锁(Monitor)的偏向锁、轻量级锁都使用了CAS自旋,只有在竞争超过阈值时才升级为重量级锁(阻塞)。
Q4:如何手动实现一个可重入自旋锁?
答:加一个持有者线程计数器,如果当前线程是持有者,则直接重入;否则进入自旋,关键在于用ThreadLocal或成员变量记录持有者。
自旋锁是Java并发编程中的“轻骑兵”,它牺牲CPU换取响应速度,是理解AQS和并发包内部机制的基础,掌握它不仅有助于应对面试,更能帮你在高并发场景下写出“极致性能”的代码。但切记:无脑使用自旋锁,是对服务器CPU的“谋杀”。