自旋锁与阻塞锁

最近在学锁相关的内容,有些内容比较纠结收集了很多资料才建立起锁相关这个体系的轮廓。

首先是操作系统线程5状态图,java虚拟机也可大致类比。

首先一个线程从就绪状态到运行状态,从运行状态到阻塞状态都要经过从用户态与内核态之间的切换,这一部比较耗时因为需要经过线程上下文数据的保存。

img

自旋锁与阻塞锁耗时大致一样?

这是我最开始非常纠结的问题。

阻塞锁

阻塞锁是指当线程尝试获取锁失败时,线程进入阻塞状态,直到接收信号后被唤醒.很明显这就符合从用户态切换到内核态

那这个开销成本是什么呢?会有两次线程上下文切换的成本

  • 当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行;
  • 接着,当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运行。

线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。

上下文切换需要几十纳秒到几微秒之间,如果锁住的代码执行时间极短(常见情况),那花在两次上下文切换的时间就会远多于锁住代码的执行时长。而且,线程的私有数据已经在CPU的cache上都预热好了,这一出一进,数据可能就凉透了,之后反复的cache miss那可就真的酸爽。

自旋锁

java中自旋锁的实现是使用CAS(Compare And Swap),他所使用的汇编命令是 cmpxchg,是用硬件通过锁住cpu到内存总线的一种方式实现的一个指令。

在CAS中,有这样三个值:
●V:变量var,也即AtomicInteger类当中被声明为volatile 的value
●E:期望值(expected)
●U:新值(update)
比较并交换的过程如下:
判断V是否等于E,如果等于,将V的值设置为U;如果不等,说明已经有其它线程更新了变量v,则当前线程放弃更新,什么都不做。

至于自旋呢,看字面意思也很明白,自己旋转,是指尝试获取锁的线程不会立即阻塞而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。
●这样的好处是减少线程上下文切换的消耗,
●缺点是循环会消耗CPU。

java中AtomicInteger类中使用自旋锁相关案例的源码如下:

image-20230212111931633

image-20230212111845603

最开始我纠结的一点是为什么自旋锁可以减少线程上下文切换的消耗。我走到了一个死胡同是这么想的。阻塞锁中进一步运行的条件是另外一个线程释放该锁,那么自旋锁想接着运行有效代码肯定也要等待其他线程释放该锁呀!这还不是要把自己的线程切换给别的线程去进行释放锁?这不是和阻塞锁一样了都要进行线程的切换。其实你想想在多核系统下就不用了呀 两个线程运行在不同核,你可以任凭在其中一个核空转cpu,而另外一个线程在其他核运行。这样并不会进行线程切换呀。所以自旋锁只在多核cpu上才能发挥作用

单核下需不需要锁?

自旋锁我想通后,又走入一个误区。单核下每个线程的切换都是串行的,轮流使用一个核心。切换时资源都会同步到内存中应该就不要锁来锁住共享变量了吧?不对不对。其实每个线程都有自己的工作内存!!他会把共享变量复制到自己的工作内存来进行计算!!所以你看线程切换时可能双方工作内存中保存的那个共享变量就可能不一致了,还是要加锁。

手写自旋锁

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
44
45
46
47
48
49
50
51
52
53
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
* 题目:实现一个自旋锁,复习CAS思想
* 自旋锁好处:循环比较获取没有类似wait的阻塞。
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
* 当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到。
*/
public class SpinLockDemo
{
AtomicReference<Thread> atomicReference = new AtomicReference<>(); //里面可以保证cas操作

public void lock()
{
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
while (!atomicReference.compareAndSet(null, thread)) {

}
}

public void unLock()
{
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t"+"----task over,unLock...");
}

public static void main(String[] args)
{
SpinLockDemo spinLockDemo = new SpinLockDemo();

new Thread(() -> {
spinLockDemo.lock();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
spinLockDemo.unLock();
},"A").start();

//暂停500毫秒,线程A先于B启动
try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }

new Thread(() -> {
spinLockDemo.lock();

spinLockDemo.unLock();
},"B").start();


}
}

自旋锁与阻塞锁
https://lililib.github.io/自旋锁与阻塞锁/
作者
煨酒小童
发布于
2023年2月12日
许可协议