1. 什么是自旋锁
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成 busy-waiting。
2.Java 如何实现自旋锁?
先看一个实现自旋锁的例子,java.util.concurrent 包里提供了很多面向并发编程的类。使用这些类在多核 CPU 的机器上会有比较好的性能。主要原因是这些类里面大多使用 (失败 - 重试方式的) 乐观锁而不是 synchronized 方式的悲观锁.
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 方法释放了该锁。
自旋锁验证代码
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,然后创建两个线程,分别执行,结果如下:
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 实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在 “线程饥饿” 问题。