java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java自旋锁

Java中的自旋锁解析

作者:李长渊哦

这篇文章主要介绍了Java中的自旋锁解析,自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态,需要的朋友可以参考下

一、自旋锁介绍

什么是自旋锁

自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

为什么要使用自旋锁

多个线程对同一个变量一直使用CAS操作,那么会有大量修改操作,从而产生大量的缓存一致性流量,因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能。

线程自旋与线程阻塞

阻塞的缺点显而易见,线程一旦进入阻塞(Block),再被唤醒的代价比较高,性能较差。自旋的优点是线程还是Runnable的,只是在执行空代码。当然一直自旋也会白白消耗计算资源,所以常见的做法是先自旋一段时间,还没拿到锁就进入阻塞。JVM在处理synchrized实现时就是采用了这种折中的方案,并提供了调节自旋的参数。

首先来对比一下互斥锁和自旋锁。

虽然自旋锁效率比互斥锁高,但它会存在下面两个问题

  1. 自旋锁一直占用CPU,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致CPU效率降低。
  2. 试图递归地获得自旋锁会引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。

由此可见,我们要慎重的使用自旋锁,自旋锁适合于锁使用者保持锁时间比较短并且锁竞争不激烈的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。

二、代码举例

/**
 * @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时间。

如果线程竞争不激烈,并且保持锁的时间很短,则适合使用自旋锁。

到此这篇关于Java中的自旋锁解析的文章就介绍到这了,更多相关Java自旋锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文