0%

自旋锁的原理和优缺点

1. 自旋锁(spin lock)

1.1 背景

同一时刻只能有一个线程获取到锁。那么就面临一个问题,那么没有获取到锁的线程应该怎么办?通常有两种处理方式:

一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁叫做自旋锁。

还有一种处理方式就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁。

1.2 原理

自旋锁不会导致线程的状态切换(用户态->内核态),一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。

由于自旋时不释放CPU,如果持有自旋锁的线程一直不释放自旋锁,那么等待该自旋锁的线程会一直浪费CPU时间。因此,自旋锁主要适用于被持有时间短,线程不希望在重新调度上花过多时间的情况。

优点:减少了不必要的上下文切换,执行速度快。

缺点:一直浪费CPU时间。

所以自旋锁适用于锁持有时间非常短的场景。

1
2
3
4
5
6
7
8
9
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);
}
}

1.3 可重入自旋锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ReentrantSpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
private int count;
public void lock() {
Thread current = Thread.currentThread();
if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
count++;
return;
}
// 如果没获取到锁,则通过CAS自旋
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread cur = Thread.currentThread();
if (cur == cas.get()) {
if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
count--;
} else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
cas.compareAndSet(cur, null);
}
}
}
}

1.4 golang实现自旋锁

参考:https://github.com/tidwall/spinlock

1
2
3
4
5
6
7
8
9
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
43
package main

import (
"fmt"
"runtime"
"sync/atomic"
"time"
)

type SpinLock uint32

func (s *SpinLock) Lock() {
for !atomic.CompareAndSwapUint32((*uint32)(s), 0, 1) {
runtime.Gosched() // 不要占满整个CPU,让出时间片
}
}

func (s *SpinLock) Unlock() {
atomic.StoreUint32((*uint32)(s), 0)
}

func main() {
var lock SpinLock
lock.Lock() // 获取锁

// 模拟共享资源的操作
go func() {
lock.Lock()
fmt.Println("lock 2")
lock.Unlock()
}()

fmt.Println("lock 1")
time.Sleep(time.Second * 2) // 模拟长时间操作
lock.Unlock() // 释放锁

time.Sleep(time.Minute)
}
/*
lock 1
// 2s...
lock 2
*/

1.5 自旋锁问题

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

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

2. 参考资料

可以加首页作者微信,咨询相关问题!