文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

Java 锁相关详解【五、Java 并发中的自旋锁与无锁编程】

Java 并发中的自旋锁与无锁编程

在前几篇文章中,我们讨论了 synchronizedReentrantLockReadWriteLockStampedLock 等锁机制。它们的共同点是 线程在竞争不到锁时,会被阻塞,等待被唤醒
但是阻塞/唤醒操作涉及 用户态与内核态切换,代价非常昂贵。

为了减少这种上下文切换的开销,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 中通过 UnsafeVarHandle 提供 CAS 操作,JUC 的 Atomic 系列类就是基于 CAS 实现的。

2.3 常见的无锁数据结构

  1. 原子计数器

    AtomicInteger counter = new AtomicInteger(0);
    counter.incrementAndGet(); // CAS 实现
    
  2. 无锁栈(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; }
        }
    }
    
  3. 无锁队列(Michael-Scott Queue):JDK 中的 ConcurrentLinkedQueue 实现。


三、自旋锁 vs 无锁编程

特性自旋锁无锁编程 (CAS)
阻塞/非阻塞可能阻塞(最终可能进入重量级锁)完全非阻塞
实现方式自旋等待 + CASCAS 原子操作
性能特点锁持有时间短时高效乐观并发,高度扩展性
风险CPU 空转浪费ABA 问题(需配合 AtomicStampedReference
使用场景轻量级锁、锁竞争低高并发下的队列、计数器、缓存系统

四、工程实践与优化

  1. 自旋锁

    • 仅在锁持有时间极短时使用。
    • JVM 默认的偏向锁与轻量级锁已内置自旋机制,应用层极少需要手动实现。
  2. 无锁编程

    • 尽量使用 JUC 提供的原子类和并发容器(如 AtomicIntegerConcurrentLinkedQueue)。
    • 避免自己实现复杂的 lock-free 数据结构,除非对性能有极致要求。
  3. ABA 问题

    • CAS 可能出现 ABA 问题(值从 A 变为 B 再变回 A,CAS 不可察觉)。
    • 解决方法:使用 版本号AtomicStampedReference
  4. 性能评估

    • 在低并发下,自旋锁/无锁未必优于传统锁。
    • 在高并发、低延迟场景下,CAS 更加合适。

五、总结

  • 自旋锁:利用忙等待减少线程切换,适合短期锁竞争。
  • 无锁编程:基于 CAS 原子操作,提供非阻塞的并发控制,更具可扩展性。
  • JVM 内置锁优化:偏向锁、轻量级锁本质上都使用了自旋与 CAS 技术。
  • 工程实践:优先使用 JUC 提供的高性能并发类,而非自行造轮子。

一句话总结:
自旋锁与无锁编程是现代 Java 并发性能优化的核心基石,它们让我们能够在极端高并发下,避免传统锁的上下文切换开销,实现近乎线性扩展的性能表现。

posted @ 2025-09-04 16:54  NeoLshu  阅读(9)  评论(0)    收藏  举报  来源