一、自旋锁介绍
- 什么是自旋锁
自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。 - 为什么要使用自旋锁
多个线程对同一个变量一直使用CAS操作,那么会有大量修改操作,从而产生大量的缓存一致性流量,因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能。 - 线程自旋与线程阻塞
阻塞的缺点显而易见,线程一旦进入阻塞(Block),再被唤醒的代价比较高,性能较差。自旋的优点是线程还是Runnable的,只是在执行空代码。当然一直自旋也会白白消耗计算资源,所以常见的做法是先自旋一段时间,还没拿到锁就进入阻塞。JVM在处理synchrized实现时就是采用了这种折中的方案,并提供了调节自旋的参数。 - 首先来对比一下互斥锁和自旋锁。
互斥锁:从等待到解锁过程,线程会从block状态变为running状态,过程中有线程上下文的切换,抢占CPU等开销。
自旋锁:从等待到解锁过程,线程一直处于running状态,没有上下文的切换。 - 虽然自旋锁效率比互斥锁高,但它会存在下面两个问题
- 自旋锁一直占用CPU,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致CPU效率降低。
- 试图递归地获得自旋锁会引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。
由此可见,我们要慎重的使用自旋锁,自旋锁适合于锁使用者保持锁时间比较短并且锁竞争不激烈的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
二、代码举例
/**
* @author lichangyuan
* @create 2021-10-08 10:50
*/
public class SpinLock {
public static void main(String[] args) throws InterruptedException {
//设置100容量线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
//计数器用于阻塞
CountDownLatch countDownLatch = new CountDownLatch(10);
//创建自旋锁对象
SimpleSpinningLock simpleSpinningLock = new SimpleSpinningLock();
for (int i = 0; i < 10; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
simpleSpinningLock.lock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程:" + Thread.currentThread().getName() + "执行");
simpleSpinningLock.unLock();
//确认已经连接完毕后再进行操作,将count值减1
countDownLatch.countDown();
}
});
}
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行,没有则调用countDown则继续阻塞
countDownLatch.await();
}
}
class SimpleSpinningLock {
/**
* 持有锁的线程,null表示锁未被线程持有
*/
private AtomicReference<Thread> sign = new AtomicReference<>();
/**
* 调用lock方法时,如果sign当前值为null,说明自旋锁还没有被占用,将sign设置为currentThread,并进行锁定。
* 调用lock方法时,如果sign当前值不为null,说明自旋锁已经被其他线程占用,当前线程就会在while中继续循环检测。
*/
public void lock() {
//返回的正是执行当前代码指令的线程引用
Thread currentThread = Thread.currentThread();
//expect:它指定原子对象应为的值。
//val:如果原子整数等于期望值,则该值指定要更新的值。
while (!sign.compareAndSet(null, currentThread)) {
//当ref为null的时候compareAndSet返回true,反之为false
//通过循环不断的自旋判断锁是否被其他线程持有
}
}
/**
* 调用unlock方法时,会将sign置为空,相当于释放自旋锁。
*/
public void unLock() {
Thread currentThread = Thread.currentThread();
//expect:它指定原子对象应为的值。
//val:如果原子整数等于期望值,则该值指定要更新的值。
sign.compareAndSet(currentThread, null);
}
}
总结
由于自旋锁只是在当前线程不停地执行循环体,不进行线程状态的切换,因此响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要占用CPU时间。如果线程竞争不激烈,并且保持锁的时间很短,则适合使用自旋锁。