27-什么是自旋锁?自旋的好处和后果是什么呢?

什么是自旋

首先,我们了解什么叫自旋?“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环。“自旋”就是自己在这里不停地循环,直到目标达成。而不像普通的锁那样,如果获取不到锁就进入阻塞。

对比自旋和非自旋的获取锁的流程

下面我们用这样一张流程图来对比一下自旋锁和非自旋锁的获取锁的过程。
在这里插入图片描述
首先,我们来看自旋锁,它并不会放弃 CPU 时间片,而是通过自旋等待锁的释放,也就是说,它会不停地再次地尝试获取锁,如果失败就再次尝试,直到成功为止。

我们再来看下非自旋锁,非自旋锁和自旋锁是完全不一样的,如果它发现此时获取不到锁,它就把自己的线程切换状态,让线程休眠,然后 CPU 就可以在这段时间去做很多其他的事情,直到之前持有这把锁的线程释放了锁,于是 CPU 再把之前的线程恢复回来,让这个线程再去尝试获取这把锁。如果再次失败,就再次让线程休眠,如果成功,一样可以成功获取到同步资源的锁。

可以看出,非自旋锁和自旋锁最大的区别,就是如果它遇到拿不到锁的情况,它会把线程阻塞,直到被唤醒。而自旋锁会不停地尝试。那么,自旋锁这样不停尝试的好处是什么呢?

自旋锁的好处

首先,阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大。

在很多场景下,可能我们的同步代码块的内容并不多,所以需要的执行时间也很短,如果我们仅仅为了这点时间就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,等待其他线程释放锁,有时我只需要稍等一下,就可以避免上下文切换等开销,提高了效率。

用一句话总结自旋锁的好处,那就是自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。

AtomicLong 的实现

在 Java 1.5 版本及以上的并发包中,也就是 java.util.concurrent 的包中,里面的原子类基本都是自旋锁的实现。

比如我们看一个 AtomicLong 的实现,里面有一个 getAndIncrement 方法,源码如下:

public final long getAndIncrement() {
    return unsafe.getAndAddLong(this, valueOffset, 1L);
}

可以看到它调用了一个 unsafe.getAndAddLong,所以我们再来看这个方法:

public final long getAndAddLong (Object var1,long var2, long var4){
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
    return var6;
}

在这个方法中,它用了一个 do while 循环。这里就很明显了:

do {
    var6 = this.getLongVolatile(var1, var2);
} 
while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

这里的 do-while 循环就是一个自旋操作,如果在修改过程中遇到了其他线程竞争导致没修改成功的情况,就会 while 循环里进行死循环,直到修改成功为止。

自己实现一个可重入的自旋锁

下面我们来看一个自己实现可重入的自旋锁。

代码如下所示:

package lesson27;
 
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
 
/**
 * 描述:     实现一个可重入的自旋锁
 */
public class ReentrantSpinLock  {
 
    private AtomicReference<Thread> owner = new AtomicReference<>();
 
    //重入次数
    private int count = 0;
 
    public void lock() {
        Thread t = Thread.currentThread();
        if (t == owner.get()) {
            ++count;
            return;
        }
        //自旋获取锁
        while (!owner.compareAndSet(null, t)) {
            System.out.println("自旋了");
        }
    }
 
    public void unlock() {
        Thread t = Thread.currentThread();
        //只有持有锁的线程才能解锁
        if (t == owner.get()) {
            if (count > 0) {
                --count;
            } else {
                //此处无需CAS操作,因为没有竞争,因为只有线程持有者才能解锁
                owner.set(null);
            }
        }
    }
 
    public static void main(String[] args) {
        ReentrantSpinLock spinLock = new ReentrantSpinLock();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
                spinLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

这段代码的运行结果是:

...
自旋了
自旋了
自旋了
自旋了
自旋了
自旋了
自旋了
自旋了
Thread-0释放了了自旋锁
Thread-1获取到了自旋锁

前面会打印出很多“自旋了”,说明自旋期间,CPU依然在不停运转。

缺点

那么自旋锁有没有缺点呢?其实自旋锁是有缺点的。它最大的缺点就在于虽然避免了线程切换的开销,但是它在避免线程切换开销的同时也带来了新的开销,因为它需要不停得去尝试获取锁。如果这把锁一直不能被释放,那么这种尝试只是无用的尝试,会白白浪费处理器资源。也就是说,虽然一开始自旋锁的开销低于线程切换,但是随着时间的增加,这种开销也是水涨船高,后期甚至会超过线程切换的开销,得不偿失。

适用场景

所以我们就要看一下自旋锁的适用场景。首先,自旋锁适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率。

可是如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源。

<think>首先,用户的问题是:“rcu为什么会阻塞自旋锁?”这是一个关于Linux内核同步机制的问题。用户之前讨论过IP冲突模块中的死锁问题,涉及RCU(Read-Copy-Update)自旋锁。 我的回答需要: 1. 用中文回答。 2. 回答后,必须提出3-5个相关问题,格式为“§§相关问题§§”,第一行必须是完整的“§§相关问题§§”,不能换行分散。 3. 问题必须基于用户的问题我的回答。 理解问题:RCU为什么会阻塞自旋锁?实际上,RCU本身不会直接阻塞自旋锁。在之前的上下文中,死锁是由于在持有自旋锁时调用RCU回调,导致软中断被阻塞,从而引起死锁。 关键点: - RCU是一种同步机制,允许无锁读取,但更新需要处理回调。 - 自旋锁是一种忙等待锁,持有时会禁用抢占。 - 问题在于:当线程持有自旋锁时,调用了call_rcu(),这会安排回调在软中断上下文运行。但软中断需要等到自旋锁释放后才能运行,而自旋锁持有者在等待RCU回调完成(在某些情况下),形成死锁。 所以,回答应该解释: - 自旋锁的特性:持有自旋锁时,当前CPU的抢占被禁用,软中断不能在该CPU上运行。 - RCU回调通常在软中断上下文执行。 - 因此,如果持有自旋锁的线程触发RCU回调,回调无法运行,因为它需要同一个CPU或相关资源,导致阻塞。 结构化回答: 1. 解释自旋锁的行为。 2. 解释RCU回调的执行上下文。 3. 描述死锁场景。 4. 总结为什么这会导致阻塞。 然后,生成相关问题。基于这个主题,提出3-5个问题。例如: 1. RCU的回调在什么上下文中执行? 2. 如何避免在持有自旋锁时调用可能触发阻塞的函数? 3. 在Linux内核中,RCU自旋锁的主要区别是什么? 4. 如何调试由RCU锁引起的内核死锁? 5. 是否有替代方案可以在持有锁时安全使用RCU? 确保问题数量在3到5个之间。 最后,严格遵守格式:回答结束后,立即以“§§相关问题§§”开头,然后列出问题。 </think> ### RCU为什么会阻塞自旋锁? 在Linux内核中,**RCU(Read-Copy-Update)本身不会直接阻塞自旋锁**,但二者结合使用时可能因执行上下文锁特性引发阻塞死锁,具体原因如下: --- #### 1. **自旋锁的特性导致阻塞风险** - **非休眠特性**:自旋锁(spinlock)是**忙等待锁**,持有期间会**禁用CPU抢占**(`preempt_disable()`),并阻止软中断(softirq)在当前CPU核上运行。 - **关键限制**:在持有自旋锁时,内核不能切换上下文或处理异步事件(如软中断、RCU回调)。 #### 2. **RCU回调的执行机制** - **异步触发**:调用 `call_rcu()` 时,RCU回调函数不会立即执行,而是由**RCU软中断**(通常在 `softirq` 上下文)在安全时异步调用。 - **依赖条件**:RCU软中断需要等待所有持有RCU读临界区的CPU核退出,且**要求当前CPU核可响应中断**。 #### 3. **死锁触发场景** 当线程在持有自旋锁时触发RCU回调: ```c SPIN_LOCK(&lock); // 1. 禁用抢占,阻塞软中断 call_rcu(&rcu_head, callback); // 2. 请求RCU回调 SPIN_UNLOCK(&lock); // 3. 解锁后回调才可能执行 ``` - **死锁形成**: - **步骤1**:线程A持有自旋锁 → **软中断被禁用**。 - **步骤2**:`call_rcu()` 注册回调 → 回调需通过软中断执行。 - **步骤3**:软中断因抢占禁用而**无法运行**,等待线程A释放CPU。 - **结果**:线程A在等RCU完成(隐式),RCU在等线程A释放锁 → **死锁**。 #### 4. **根本原因** - **层级冲突**:自旋锁属于**原子操作层**(原子上下文),RCU回调属于**软中断层**(可休眠上下文)。 - **资源互斥**:自旋锁阻塞软中断运行,而RCU回调依赖软中断,形成**循环等待**。 --- ### 规避方案 - **锁替换**:在进程上下文使用**互斥锁**(mutex)代替自旋锁,因mutex允许休眠抢占。 - **延迟销毁**:将 `call_rcu()` 移出锁保护区,确保回调注册在无锁环境下执行。 - **同步机制**:必要时用 `synchronize_rcu()` 显式等待RCU宽限期结束(但需避免在锁内调用)。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值