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(); while (!cas.compareAndSet(null, current)) { 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; } while (!cas.compareAndSet(null, current)) { } } public void unlock() { Thread cur = Thread.currentThread(); if (cur == cas.get()) { if (count > 0) { count--; } else { 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() } }
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) }
|
1.5 自旋锁问题
如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗 CPU。使用不当会造成 CPU 使用率极高。
上面 Java 实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在 “线程饥饿” 问题。
2. 参考资料