深入理解自旋锁

3,398 阅读5分钟

转自

zhuanlan.zhihu.com/p/40729293

juejin.cn/post/684490…

首先简单回顾一下CAS算法

CAS算法 Compare And Swap , 是一种典型的无锁算法,即在不使用锁的情况下实现多线程之间变量的同步,因为没有锁,因此CAS可以保证线程在没有阻塞的情况下实现,因此也叫做非阻塞同步(Non-blocking Synchronization)。 CAS的实现涉及到三个操作数:

  • 需要读写的内存值 V
  • 期望值 A
  • 拟写入的值 B

当且仅当V==A时,CAS才通过原子方式用新值B来更新V的值,否则不会进行任何操作(值得一提的是在这里Compare 和 Swap都是原子操作)。

CAS的过程可以用以下代码来表示:

public boolean compareAndSwap(int value, int expect, int update) {
//        如果内存中的值value和期望值expect一样 则将值更新为新值update
    if (value == expect) {
        value = update;
        return true;
    } else {
        return false;
    }
}

CAS的缺点:ABA问题

什么是ABA问题?

假设有三个线程:

  • 线程1: 期望值A , 欲更新值B

  • 线程2: 期望值A , 欲更新值B

  • 线程3: 期望值B, 欲更新值A

假设线程本来应该以1->2->3的顺序执行,但如果线程2因为其他原因阻塞了,最终线程以1->3 ->2 的顺序执行,那么线程2其实不知道已经发生了A->B->A的过程。

ABA的危害

小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50

线程1(提款机):获取当前值100,期望更新为50,

线程2(提款机):获取当前值100,期望更新为50,

线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50

线程3(默认):获取当前值50,期望更新为100,

这时候线程3成功执行,余额变为100,

线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!!

此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。

如何解决?

在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A

什么是自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

Java如何实现自旋锁

public class SpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}

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

自旋锁存在的问题

  • 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。

  • 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点

  • 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快

  • 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。(线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

自旋锁与互斥锁的区别

  • 自旋锁与互斥锁都是为了实现保护资源共享的机制。

  • 无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。

  • 获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。

总结

  • 自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。

  • 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。

  • 自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。

  • 自旋锁本身无法保证公平性,同时也无法保证可重入性。

  • 基于自旋锁,可以实现具备公平性和可重入性质的锁。