首页 > 编程语言 >Java中的自旋锁,手动实现一个自旋锁

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

时间:2023-03-13 22:38:40浏览次数:29  
标签:开销 Java Thread 手动 获取 线程 自旋 CPU

自旋锁

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

自旋锁 & 非自旋锁

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

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

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

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

2022-12-18-18-19-19.png

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

自旋锁有啥好处?

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

自旋锁有啥坏处?

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

适用场景

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

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

Java中的自旋锁

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

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

2022-12-18-18-39-55.png

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

2022-12-18-18-40-45.png

手写一个可重入的自旋锁

为了引入自旋特性,我们使用 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();
    }
}

2022-12-18-18-27-29.png

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

标签:开销,Java,Thread,手动,获取,线程,自旋,CPU
From: https://blog.51cto.com/u_16002978/6114660

相关文章

  • Linux多线程中互斥锁、读写锁、自旋锁、条件变量、信号量详解
    Hello、Hello大家好,我是ST,今天我们继续来聊一聊Linux中多线程编程中的重要知识点,详细谈谈多线程中同步和互斥机制。1、同步和互斥互斥:多线程中互斥是指多个线程访问同一资源......
  • TypeScript实例_手动编译与自动编译、类型注解、接口和类的详解
    一.认识TypeScriptTypeScript是一种由微软开发的开源、跨平台的编程语言。它是JavaScript的超集,最终会被编译为JavaScript代码。2012年10月,微软发布了首个公开版......
  • Java线程池
    线程池的目的是通过对线程的管理,让多线程程序中的多线程便捷开发、高效运行。线程池的存在的意义让线程变得可重用,减小线程创建和销毁带来的消耗。线程池中保留的可用......
  • java中的注解、自定义注解
    注解注解概述​ 注解类似于一个商品标签,给当前程序的开发者提供信息和标记,给java编译程序员或者jvm提供数据支持和标记,有着代码量少,易读性更高的好处,本质还是一个特殊的......
  • Git 仓库7K stars!学Java开源项目austin要多久?
    我是3y,一年CRUD经验用十年的markdown程序员......
  • Java数组
    Java数组1.数组概述数组的定义数组是相同类型数据的有序集合数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个数组元......
  • Java简介
    Java是什么?Java是由Sunmicrosystem公司于1995年5月推出的Java面向对象程序设计语言和Java平台的总称。Java分为三个体系:JavaSE(J2SE)(Java2platformstandardedition,J......
  • Java基础语法
    Java基础语法Java程序是一系列对象的集合,而这些对象通过调用彼此来实现协同工作。对象:对象是类的一个实例,有状态和行为。例如:一条狗是一个对象,它的状态有:颜色、名字、品......
  • Java三大集合类 - List
    ListSetMap一、List几个小问题:1、接口可以被继承吗?(可以)2、接口可以被多个类实现吗?(可以)3、以下两种写法有什么区别?//Listlist1=newList();是错误的因为List()是......
  • Java(单元测试,反射)
    单元测试、反射一、单元测试1.1单元测试快速入门所谓单元测试,就是针对最小的功能单元,编写测试代码对其进行正确性测试。我们想想,咱们之前是怎么进行测试的呢?比如说我......