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

本文介绍了自旋锁的基本概念及其与非自旋锁的区别,分析了自旋锁的优缺点,并通过Java中的AtomicLong类展示了自旋锁的实现方式。最后提供了一个可重入自旋锁的示例。

c538359fbd500f8e64713f3b19fe2c7b.jpeg

程序员的成长之路

互联网/程序员/技术/资料共享 

关注

阅读本文大概需要 4 分钟。

来自:blog.csdn.net/vincent_wen0766/article/details/108558656

什么是自旋

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

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

ebea0539d310665754c0121d5f25c23b.png

自旋锁,它并不会放弃  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 循环里进行死循环,直到修改成功为止

实现一个可重入的自旋锁示例
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 却无法拿到锁,白白消耗资源。

<END>

推荐阅读:

2022 年最佳开源软件出炉

Spring Cloud Gateway 统一异常处理

互联网初中高级大厂面试题(9个G)
内容包含Java基础、JavaWeb、MySQL性能优化、JVM、锁、百万并发、消息队列、高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper......等技术栈!
⬇戳阅读原文领取!                                  朕已阅
<think>我们正在讨论偏向锁自旋锁的区别及应用场景。根据引用内容: - 偏向锁:适用于无竞争或同一线程多次获取锁的场景,通过减少同步操作提升性能。 - 自旋锁:在锁被占用时,线程不立即阻塞而是循环等待(自旋),避免线程切换开销,适用于锁占用时间短的场景。 现在,我将整理两者的对比表格,然后分别解释其原理、区别适用场景,最后给出代码示例选择建议。</think>### Java并发编程中偏向锁与自旋锁的区别及应用场景 #### 一、核心区别对比 | **特性** | **偏向锁 (Biased Locking)** | **自旋锁 (Spin Lock)** | |-------------------|-----------------------------------------------|-------------------------------------------| | **优化目标** | 减少无竞争时的锁获取开销 | 减少线程阻塞/唤醒开销 | | **工作原理** | 记录第一个获取锁的线程ID,后续直接访问[^1] | 循环检测锁状态(while循环)[^2] | | **锁状态** | 属于对象头Mark Word的特殊状态 | 无特定状态,是一种等待策略 | | **升级路径** | 无竞争→偏向锁→轻量级锁→重量级锁 | 不改变锁状态,是重量级锁的等待策略 | | **适用场景** | 单线程重复获取锁的场景 | 锁持有时间极短(<纳秒级)的场景 | | **资源消耗** | 几乎零开销(无CAS操作) | CPU占用高(空转消耗CPU周期) | | **JDK版本** | Java 6+ | 所有版本 | | **禁用方式** | `-XX:-UseBiasedLocking` | `-XX:+PreBlockSpin`(调整自旋次数) | #### 二、实现原理详解 1. **偏向锁工作原理** ```mermaid graph LR A[线程首次获取锁] --> B[对象头记录线程ID] B --> C[进入偏向模式] C --> D{同一线程再次请求} D -->|是| E[直接访问无需同步] D -->|否| F[升级为轻量级锁] ``` - 通过对象头Mark Word存储线程ID(23位)epoch(2位) - 同一线程再次获取锁时跳过CAS操作[^1] 2. **自旋锁实现机制** ```java // 伪代码实现 while (!tryAcquireLock()) { // 空循环消耗CPU周期 int spinCount = 0; while (spinCount++ < MAX_SPINS && !lockAvailable()) { // 空转等待 } // 超过自旋上限后加入等待队列 enqueue(currentThread); } ``` - 默认自旋次数:JDK 6前固定10次,JDK 6+启用自适应自旋JVM动态调整) #### 三、适用场景对比 | **场景类型** | **推荐锁类型** | **原因说明** | |---------------------------|----------------|----------------------------------------------| | 单线程重复操作(如初始化)| 偏向锁 | 避免无谓的CAS操作开销[^1] | | 短时同步块(<10ns) | 自旋锁 | 避免线程切换开销(约5-10μs)[^2] | | 高并发读写(如缓存) | 自旋锁 | 减少上下文切换提升吞吐量 | | 长时间临界区(>1ms) | 禁用自旋锁 | 防止CPU空转浪费资源 | | 线程竞争激烈场景 | 禁用偏向锁 | 锁撤销开销 > 优化收益(默认开启)[^1] | #### 四、代码示例分析 ```java // 偏向锁优势场景 public class Singleton { private static Singleton instance; public synchronized static Singleton getInstance() { if (instance == null) { instance = new Singleton(); // 初始化后偏向锁生效 } return instance; // 后续调用直接访问 } } // 自旋锁适用场景 AtomicInteger counter = new AtomicInteger(0); void increment() { int current; do { current = counter.get(); // 自旋直到CAS成功(适用于高并发计数) } while (!counter.compareAndSet(current, current + 1)); } ``` #### 五、最佳实践建议 1. **启用偏向锁的条件** - 确定单线程主导场景(如事件分发线程) - 添加JVM参数:`-XX:BiasedLockingStartupDelay=0`(禁用4秒延迟) 2. **自旋锁调优策略** ```bash # 调整自旋参数(JDK 6+) -XX:PreBlockSpin=20 # 设置最大自旋次数 -XX:+UseSpinning # 显式启用自旋 ``` 3. **场景化选择指南** | **场景** | **推荐方案** | |---------------------------|----------------------------| | 单线程循环访问 | 偏向锁 | | 微秒级操作(计数器) | CAS自旋 + Atomic类 | | 毫秒级操作(数据库连接) | 重量级锁 + 阻塞队列 | | 不可预测的竞争 | 禁用偏向锁(默认) | > **性能陷阱**:在容器环境(如Docker)中,偏向锁可能因线程ID复用导致性能下降,需通过`-XX:-UseBiasedLocking`显式禁用。 #### 六、锁升级流程 ```mermaid graph TB A[无锁状态] -->|线程首次获取| B[偏向锁] B -->|其他线程竞争| C[轻量级锁] C -->|自旋失败/长时间等待| D[重量级锁] D -->|线程阻塞| E[操作系统调度] ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值