自旋锁是什么

自旋锁 (Spin Lock) 是一种基础的、非阻塞的同步原语,用于在多处理器(或多核)环境中保护共享资源。当一个线程尝试获取一个已被其他线程持有的自旋锁时,该线程并不会立即被阻塞(即放弃CPU,进入睡眠状态),而是会持续地循环检查(“自旋”)该锁是否已经被释放。一旦锁被释放,该线程会立即获取锁并继续执行。

自旋锁的核心思想是:如果锁的持有时间非常短,那么线程忙等待(自旋)的开销可能比线程阻塞和唤醒的开销(涉及上下文切换、内核态转换等)要小。

特点:

  1. 忙等待 (Busy-Waiting):尝试获取锁的线程会占用CPU时间进行循环检查,而不是让出CPU。
  2. 非阻塞(指线程本身不进入阻塞态):虽然线程在“等待”锁,但它仍然处于运行状态,不断消耗CPU周期。
  3. 适用于锁持有时间极短的场景:如果锁的持有时间很长,自旋会导致CPU资源的大量浪费。
  4. 通常在多核处理器上使用:在单核处理器上,如果持有锁的线程和尝试获取锁的线程在同一个CPU上,自旋是没有意义的,因为自旋线程会一直占用CPU,导致持有锁的线程无法运行并释放锁(除非发生抢占,但自旋的目的就是避免上下文切换)。因此,自旋锁主要用于多核系统,期望持有锁的线程在另一个核心上快速释放锁。
  5. 实现简单:自旋锁的底层实现通常依赖于原子操作,如CAS (Compare-And-Swap) 或 Test-And-Set 指令。

一个简单的自旋锁伪代码实现可能如下:

class SpinLock {
    private AtomicBoolean locked = new AtomicBoolean(false); // 原子布尔值,false表示锁未被持有

    public void lock() {
        while (!locked.compareAndSet(false, true)) {
            // 自旋,可以加入一些CPU友好的操作,如PAUSE指令 (x86) 或 YIELD (不同OS实现不同)
            // 避免过于激进的自旋导致总线风暴或过度消耗其他核的资源
        }
    }

    public void unlock() {
        locked.set(false); // 释放锁
    }
}

优点:

  • 避免上下文切换开销:当锁被持有的时间非常短时,自旋的开销可能小于线程阻塞和唤醒的开销。
  • 响应速度快:一旦锁被释放,自旋的线程可以几乎立即获取到锁并继续执行。

缺点:

  • CPU资源浪费:如果锁被长时间持有,自旋的线程会持续消耗CPU,降低系统整体效率。
  • 可能导致总线风暴:如果大量线程同时自旋等待同一个锁,频繁的内存访问(检查锁状态)可能会给内存总线带来压力。
  • 公平性问题:基本的自旋锁通常是不公平的,无法保证等待时间最长的线程先获取锁。可能会导致某些线程长时间自旋而无法获取锁(饥饿)。
  • 不适用于单核CPU:如前所述,在单核CPU上,自旋锁通常是无效甚至有害的。

使用场景:

自旋锁通常用于操作系统内核、驱动程序或对性能要求极高且锁竞争不激烈、锁持有时间极短的底层并发组件中。例如,在Linux内核中,自旋锁被广泛用于保护那些在中断上下文或持有其他锁时不能睡眠的关键数据结构。

在用户态编程中,直接使用纯粹的自旋锁较少,因为预测锁的持有时间通常比较困难。更常见的是自适应自旋锁或者混合锁:

  • 自适应自旋锁 (Adaptive Spin Lock):锁会先自旋一小段时间,如果在这段时间内没有获取到锁,再转入阻塞状态。自旋的次数或时间可以根据历史信息(例如,上次该锁是否很快被释放)动态调整。Java虚拟机中的synchronized锁优化就包含了自适应自旋。
  • 混合锁 (Hybrid Lock):结合了自旋和阻塞的特性。

与互斥锁 (Mutex) 的对比:

  • 互斥锁:当线程无法获取锁时,会进入阻塞状态,让出CPU。适用于锁持有时间可能较长或不确定的情况。
  • 自旋锁:当线程无法获取锁时,会忙等待,不让出CPU。适用于锁持有时间极短且确定的情况。

拓展延申:

  1. 自旋锁的优化:

    • PAUSE指令 (x86/x64):在自旋循环中插入PAUSE指令可以提示CPU当前处于自旋等待状态。这有助于减少CPU的功耗,并避免因过度推测执行和内存顺序冲突导致的性能惩罚。
    • 指数退避 (Exponential Backoff):如果自旋多次仍未获取到锁,可以逐渐增加自旋等待的时间间隔,或者在自旋一定次数后放弃自旋转为阻塞。
    • 票据锁 (Ticket Lock):一种公平的自旋锁实现。每个请求锁的线程获取一个唯一的“票号”,锁被释放时,持有下一个票号的线程获得锁。这避免了基本自旋锁的饥饿问题,但可能在NUMA(非一致性内存访问)架构下表现不佳,因为所有线程都在关注同一个锁状态变量。
    • MCS锁 (Mellor-Crummey and Scott Lock):一种高性能、可伸缩、公平的自旋锁。它将等待的线程组织成一个队列,每个线程只在自己的本地变量上自旋,避免了对共享锁变量的竞争,从而减少了缓存一致性流量。CLH锁是另一种类似的队列锁。
  2. Java中的自旋: Java的synchronized关键字在JDK 1.6及以后版本中引入了锁优化,其中包括自适应自旋。当一个线程尝试获取一个轻量级锁或偏向锁失败时,JVM会让它自旋一小段时间,而不是立即膨胀成重量级锁(依赖操作系统的互斥量,会导致线程阻塞)。自旋的次数是自适应的,JVM会根据历史情况判断自旋的有效性。 java.util.concurrent.locks包中的一些锁实现(如ReentrantLock)在内部也可能使用CAS操作来实现类似自旋的尝试。

  3. 死锁风险: 如果自旋锁使用不当,例如在持有自旋锁的情况下尝试获取另一个可能导致阻塞的锁,或者在中断处理程序中不当地使用自旋锁,也可能导致系统问题(虽然自旋锁本身不直接导致传统意义上的死锁,但可能导致CPU持续空转无法进展)。