理解基于CAS机制的自旋锁

本文介绍了自旋锁的概念,它是一种在多线程环境下用于同步的机制。在Java中,可以通过AtomicReference类的CAS操作实现自旋锁。自旋锁的示例代码展示了线程如何获取和释放锁,并通过一个简单的测试案例说明了其工作原理。然而,自旋锁存在潜在的问题,如长时间持有锁可能导致其他线程忙等待,消耗CPU资源,且可能存在线程饥饿问题。
摘要由CSDN通过智能技术生成

1. 什么是自旋锁
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成 busy-waiting。

2.Java 如何实现自旋锁?
先看一个实现自旋锁的例子,java.util.concurrent 包里提供了很多面向并发编程的类。使用这些类在多核 CPU 的机器上会有比较好的性能。主要原因是这些类里面大多使用 (失败 - 重试方式的) 乐观锁而不是 synchronized 方式的悲观锁.

[Java] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
class spinlock {
    private AtomicReference<Thread> cas;
    spinlock(AtomicReference<Thread> cas){
        this.cas = cas;
    }
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) { //为什么预期是null??
            // DO nothing
            System.out.println("I am spinning");
        }
    }
 
    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}



lock() 方法利用的 CAS,当第一个线程 A 获取锁的时候,能够成功获取到,不会进入 while 循环,如果此时线程 A 没有释放锁,另一个线程 B 又来获取锁,此时由于不满足 CAS,所以就会进入 while 循环,不断判断是否满足 CAS,直到 A 线程调用 unlock 方法释放了该锁。

自旋锁验证代码

[Java] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package ddx.多线程;
 
import java.util.concurrent.atomic.AtomicReference;
 
public class 自旋锁 {
    public static void main(String[] args) {
        AtomicReference<Thread> cas = new AtomicReference<Thread>();
        Thread thread1 = new Thread(new Task(cas));
        Thread thread2 = new Thread(new Task(cas));
        thread1.start();
        thread2.start();
    }
 
 
}
 
//自旋锁验证
class Task implements Runnable {
    private AtomicReference<Thread> cas;
    private spinlock slock ;
 
    public Task(AtomicReference<Thread> cas) {
        this.cas = cas;
        this.slock = new spinlock(cas);
    }
 
    @Override
    public void run() {
        slock.lock(); //上锁
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock();
    }
}


通过之前的 AtomicReference 类创建了一个自旋锁 cas,然后创建两个线程,分别执行,结果如下:

[AppleScript] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
0
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
1
I am spin
I am spin
I am spin
I am spin
I am spin
2
3
4
5
6
7
8
9
I am spin
0
1
2
3
4
5
6
7
8
9


 
通过对输出结果的分析我们可以得知,首先假定线程一在执行 lock 方法的时候获得了锁,通过方法

cas.compareAndSet(null, current)

将引用改为线程一的引用,跳过 while 循环,执行打印函数

而线程二此时也进入 lock 方法,在执行比较操作的时候发现,expect value != update value,于是进入 while 循环,打印

i am spinning。由以下红字可以得出结论,Java 中的一个线程并不是总是占着 cpu 时间片不放,一直执行完的,而是采用抢占式调度,所以出现了上面两个线程交替执行的现象

Java 线程的实现是通过映射到系统的轻量级线程上,轻量级线程有对应系统的内核线程,内核线程的调度由系统调度器来调度的,所以 Java 的线程调度方式取决于系统内核调度器,只不过刚好目前主流操作系统的线程实现都是抢占式的。

3. 自旋锁存在的问题
使用自旋锁会有以下一个问题:
1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗 CPU。使用不当会造成 CPU 使用率极高。
2. 上面 Java 实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在 “线程饥饿” 问题。

摘自http://bbs.itheima.com/thread-508070-1-1.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值