Java中的自旋锁,手动实现一个自旋锁

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的自旋锁,手动实现一个自旋锁相关的知识,希望对你有一定的参考价值。

自旋锁

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,已达到锁的效果。自旋是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁, 当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

自旋锁 & 非自旋锁

什么是自旋?字面意思是 "自我旋转" 。在 Java 中就是循环的意思,比如 for 循环,while 循环等等。那自旋锁顾名思义就是「线程循环地去获取锁」。

非自旋锁,也就是普通锁。获取不到锁,线程就进入阻塞状态。等待 CPU 唤醒,再去获取。

自旋锁 & 非自旋锁的执行流程

某线程去获取锁(可能是自旋锁 or 非自旋锁),然而锁现在被其他线程占用了。它两获取锁的执行流程就如下图所示:

  • 自旋锁:一直占用 CPU 的时间片去循环获取锁,直到获取到为止。
  • 非自旋锁:当前线程进入阻塞,CPU 可以去干别的事情。等待 CPU 唤醒了,线程才去获取非自旋锁。

自旋锁有啥好处?

  • 阻塞 & 唤醒线程都是需要资源开销的,如果线程要执行的任务并不复杂。这种情况下,切换线程状态带来的开销比线程执行的任务还要大。
  • 而很多时候,我们的任务往往比较简单,简单到线程都还没来得及切换状态就执行完毕。这时我们选择自旋锁明显是更加明智的。
  • 所以,自旋锁的好处就是用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销

自旋锁有啥坏处?

  • 虽然避免了线程切换的开销,但是它在避免线程切换开销的同时也带来了新的开销,因为它需要不停得去尝试获取锁。如果这把锁一直不能被释放,那么这种尝试只是无用的尝试,会白白浪费处理器资源。
  • 虽然刚开始自旋锁的开销大于线程切换。但是随着时间一直递增,总会超过线程切换的开销。

适用场景

自旋锁的好处就是能减少线程切换状态的开销;坏处就是如果一直旋下去,自旋开销会比线程切换状态的开销大得多。知道优缺点,那我们的适用场景就很简单了:

  • 并发不能太高,避免一直自旋不成功
  • 线程执行的同步任务不能太复杂,耗时比较短

Java中的自旋锁

在 Java 1.5 版本及以上的并发包中,也就是 java.util.concurrent 的包中,里面的原子类基本都是自旋锁的实现。我们看看做常用的 AtomicInteger 类,它里面有个 getAndIncrement 方法,源码如下:

getAndIncrement():返回当前的值,并将当前值+1

getAndIncrement 也是直接调用 usafegetAndAddInt 方法,从下面源码可以看出这个方法直接就是做了一个 do-while 的循环。「这个循环就是一个自旋操作,如果在修改过程中遇到了其他线程竞争导致没修改成功的情况,就会 while 循环里进行死循环,直到修改成功为止」。

手写一个可重入的自旋锁

为了引入自旋特性,我们使用 AtomicReference 类提供一个可以原子读写的对象引用变量。

定义一个加锁方法,如果有其他线程已经获取锁,当前线程将进入自旋,如果还是已经持有锁的线程获取锁,那就是重入

定义一个解锁方法,解锁的话,只有持有锁的线程才能解锁,解锁的逻辑思维将 count-1,如果 count == 0,则是把当前持有锁线程设置为 null,彻底释放锁。

源码如下:

public class ReentrantSpinLock 

    private AtomicReference<Thread> owner = new AtomicReference<>();

    //重入次数
    private int count = 0;

    public void lock() 
        Thread t = Thread.currentThread();
        if (t == owner.get()) 
            ++count;
            return;
        
        //自旋获取锁
        while (!owner.compareAndSet(null, t)) 
            System.out.println("自旋了");
        
    

    public void unlock() 
        Thread t = Thread.currentThread();
        //只有持有锁的线程才能解锁
        if (t == owner.get()) 
            if (count > 0) 
                --count;
             else 
                //此处无需CAS操作,因为没有竞争,因为只有线程持有者才能解锁
                owner.set(null);
            
        
    

    public static void main(String[] args) 
        ReentrantSpinLock spinLock = new ReentrantSpinLock();
        Runnable runnable = new Runnable() 
            @Override
            public void run() 
                System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
                spinLock.lock();
                try 
                    System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                    Thread.sleep(4000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                 finally 
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");
                
            
        ;
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    

从结果我们可以看出,前面一直打印 "自旋了",说明 CPU 一直在尝试获取锁。 PS:如果电脑不好的话,在这期间风扇会加速的,因为 CPU 一直在工作。

以上是关于Java中的自旋锁,手动实现一个自旋锁的主要内容,如果未能解决你的问题,请参考以下文章

Java中的锁

10.Java锁之自旋锁

Java 通过原子类 AtomicStampedReference 实现自旋锁

Java并发编程——深入理解自旋锁

Linux设备驱动程序 之 自旋锁

自旋锁