当前位置:

首页 > 编程开发 > Java怎么实现手写自旋锁

Java怎么实现手写自旋锁

前言我们在写并发程序的时候,一个非常常见的需求就是保证在某一个时刻只有一个线程执行某段代码,像这种代码叫做临界区,而通常保证一个时刻只有一个线程执行临界区的代码的方法就是锁。在本篇文章当中我们将会仔细分析和学习自旋锁,所谓自旋锁就是通过while循环实现的,让拿到锁的线程进入临界区执行代码,让没有拿到锁的线程一直进行while死循环,这其实就是线程自己“旋”在while循环了,因而这种锁就叫做自旋锁。自旋锁原子性在谈自旋锁之前就不得不谈原子性了。所谓原子性简单说来就是一个一个操作要么不做要么全做,全做的意

    前言

    我们在写并发程序的时候,一个非常常见的需求就是保证在某一个时刻只有一个线程执行某段代码,像这种代码叫做临界区,而通常保证一个时刻只有一个线程执行临界区的代码的方法就是锁。在本篇文章当中我们将会仔细分析和学习自旋锁,所谓自旋锁就是通过while循环实现的,让拿到锁的线程进入临界区执行代码,让没有拿到锁的线程一直进行while死循环,这其实就是线程自己“旋”在while循环了,因而这种锁就叫做自旋锁。

    自旋锁

    原子性

    在谈自旋锁之前就不得不谈原子性了。所谓原子性简单说来就是一个一个操作要么不做要么全做,全做的意思就是在操作的过程当中不能够被中断,比如说对变量data进行加一操作,有以下三个步骤:

    • 将data从内存加载到寄存器。

    • 将data这个值加一。

    • 将得到的结果写回内存。

    原子性就表示一个线程在进行加一操作的时候,不能够被其他线程中断,只有这个线程执行完这三个过程的时候其他线程才能够操作数据data。

    我们现在用代码体验一下,在Java当中我们可以使用AtomicInteger进行对整型数据的原子操作:

    import java.util.concurrent.atomic.AtomicInteger;
     
    public class AtomicDemo {
     
      public static void main(String[] args) throws InterruptedException {
        AtomicInteger data = new AtomicInteger();
        data.set(0); // 将数据初始化位0
        Thread t1 = new Thread(() -> {
          for (int i = 0; i < 100000; i++) {
            data.addAndGet(1); // 对数据 data 进行原子加1操作
          }
        });
        Thread t2 = new Thread(() -> {
          for (int i = 0; i < 100000; i++) {
            data.addAndGet(1);// 对数据 data 进行原子加1操作
          }
        });
        // 启动两个线程
        t1.start();
        t2.start();
        // 等待两个线程执行完成
        t1.join();
        t2.join();
        // 打印最终的结果
        System.out.println(data); // 200000
      }
    }

    从上面的代码分析可以知道,如果是一般的整型变量如果两个线程同时进行操作的时候,最终的结果是会小于200000。

    我们现在来模拟一下一般的整型变量出现问题的过程:

    主内存data的初始值等于0,两个线程得到的data初始值都等于0。

    Java怎么实现手写自旋锁

    现在线程一将data加一,然后线程一将data的值同步回主内存,整个内存的数据变化如下:

    Java怎么实现手写自旋锁

    现在线程二data加一,然后将data的值同步回主内存(将原来主内存的值覆盖掉了):

    Java怎么实现手写自旋锁

    我们本来希望data的值在经过上面的变化之后变成2,但是线程二覆盖了我们的值,因此在多线程情况下,会使得我们最终的结果变小。

    但是在上面的程序当中我们最终的输出结果是等于20000的,这是因为给data进行+1的操作是原子的不可分的,在操作的过程当中其他线程是不能对data进行操作的。这就是原子性带来的优势。

    自己动手写自旋锁

    AtomicInteger类

    现在我们已经了解了原子性的作用了,我们现在来了解AtomicInteger类的另外一个原子性的操作——compareAndSet,这个操作叫做比较并交换(CAS),他具有原子性。

    public static void main(String[] args) {
      AtomicInteger atomicInteger = new AtomicInteger();
      atomicInteger.set(0);
      atomicInteger.compareAndSet(0, 1);
    }

    compareAndSet函数的意义:首先会比较第一个参数(对应上面的代码就是0)和atomicInteger的值,如果相等则进行交换,也就是将atomicInteger的值设置为第二个参数(对应上面的代码就是1),如果这些操作成功,那么compareAndSet函数就返回true,如果操作失败则返回false,操作失败可能是因为第一个参数的值(期望值)和atomicInteger不相等,如果相等也可能因为在更改atomicInteger的值的时候失败(因为可能有多个线程在操作,因为原子性的存在,只能有一个线程操作成功)。

    自旋锁实现原理

    我们可以使用AtomicInteger类实现自旋锁,我们可以用0这个值表示未上锁,1这个值表示已经上锁了。

    AtomicInteger类的初始值为0。

    在上锁时,我们可以使用代码atomicInteger.compareAndSet(0, 1)进行实现,我们在前面已经提到了只能够有一个线程完成这个操作,也就是说只能有一个线程调用这行代码然后返回true其余线程都返回false,这些返回false的线程不能够进入临界区,因此我们需要这些线程停在atomicInteger.compareAndSet(0, 1)这行代码不能够往下执行,我们可以使用while循环让这些线程一直停在这里while (!value.compareAndSet(0, 1));,只有返回true的线程才能够跳出循环,其余线程都会一直在这里循环,我们称这种行为叫做自旋,这种锁因而也被叫做自旋锁。

    线程在出临界区的时候需要重新将锁的状态调整为未上锁的上状态,我们使用代码value.compareAndSet(1, 0);就可以实现,将锁的状态还原为未上锁的状态,这样其他的自旋的线程就可以拿到锁,然后进入临界区了。

    自旋锁代码实现

    import java.util.concurrent.atomic.AtomicInteger;
     
    public class SpinLock {
        
      // 0 表示未上锁状态
      // 1 表示上锁状态
      protected AtomicInteger value;
     
      public SpinLock() {
        this.value = new AtomicInteger();
        // 设置 value 的初始值为0 表示未上锁的状态
        this.value.set(0);
      }
     
      public void lock() {
        // 进行自旋操作
        while (!value.compareAndSet(0, 1));
      }
     
      public void unlock() {
        // 将锁的状态设置为未上锁状态
        value.compareAndSet(1, 0);
      }
     
    }

    上面就是我们自己实现的自旋锁的代码,这看起来实在太简单了,但是它确实帮助我们实现了一个锁,而且能够在真实场景进行使用的,我们现在用代码对上面我们写的锁进行测试。

    测试程序:

    public class SpinLockTest {
     
      public static int data;
      public static SpinLock lock = new SpinLock();
     
      public static void add() {
        for (int i = 0; i < 100000; i++) {
          // 上锁 只能有一个线程执行 data++ 操作 其余线程都只能进行while循环
          lock.lock();
          data++;
          lock.unlock();
        }
      }
     
      public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[100];
        // 设置100个线程
        for (int i = 0; i < 100; i ++) {
          threads[i] = new Thread(SpinLockTest::add);
        }
        // 启动一百个线程
        for (int i = 0; i < 100; i++) {
          threads[i].start();
        }
        // 等待这100个线程执行完成
        for (int i = 0; i < 100; i++) {
          threads[i].join();
        }
        System.out.println(data); // 10000000
      }
    }

    在上面的代码单中,我们使用100个线程,然后每个线程循环执行100000data++操作,上面的代码最后输出的结果是10000000,和我们期待的结果是相等的,这就说明我们实现的自旋锁是正确的。

    自己动手写可重入自旋锁

    可重入自旋锁

    在上面实现的自旋锁当中已经可以满足一些我们的基本需求了,就是一个时刻只能够有一个线程执行临界区的代码。但是上面的的代码并不能够满足重入的需求,也就是说上面写的自旋锁并不是一个可重入的自旋锁,事实上在上面实现的自旋锁当中重入的话就会产生死锁。

    我们通过一份代码来模拟上面重入产生死锁的情况:

    public static void add(int state) throws InterruptedException {
      TimeUnit.SECONDS.sleep(1);
      if (state <= 3) {
        lock.lock();
        System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state);
        for (int i = 0; i < 10; i++)
          data++;
        add(state + 1); // 进行递归重入 重入之前锁状态已经是1了 因为这个线程进入了临界区
        lock.unlock();
      }
    }

    在上面的代码当中加入我们传入的参数state的值为1,那么在线程执行for循环之后再次递归调用add函数的话,那么state的值就变成了2。

    if条件仍然满足,这个线程也需要重新获得锁,但是此时锁的状态是1,这个线程已经获得过一次锁了,但是自旋锁期待的锁的状态是0,因为只有这样他才能够再次获得锁,进入临界区,但是现在锁的状态是1,也就是说虽然这个线程获得过一次锁,但是它也会一直进行while循环而且永远都出不来了,这样就形成了死锁了。

    可重入自旋锁思想

    针对上面这种情况我们需要实现一个可重入的自旋锁,我们的思想大致如下:

    • 在我们实现的自旋锁当中,我们可以增加两个变量,owner一个用于存当前拥有锁的线程,count一个记录当前线程进入锁的次数。

    • 如果线程获得锁,owner = Thread.currentThread()并且count = 1。

    • 当线程下次再想获取锁的时候,首先先看owner是不是指向自己,则一直进行循环操作,如果是则直接进行count++操作,然后就可以进入临界区了。

    • 我们在出临界区的时候,如果count大于一的话,说明这个线程重入了这把锁,因此不能够直接将锁设置为0也就是未上锁的状态,这种情况直接进行count--操作,如果count等于1的话,说明线程当前的状态不是重入状态(可能是重入之后递归返回了),因此在出临界区之前需要将锁的状态设置为0,也就是没上锁的状态,好让其他线程能够获取锁。

    可重入锁代码实现

    实现的可重入锁代码如下:

    public class ReentrantSpinLock extends SpinLock {
     
      private Thread owner;
      private int count;
     
      @Override
      public void lock() {
        if (owner == null || owner != Thread.currentThread()) {
          while (!value.compareAndSet(0, 1));
          owner = Thread.currentThread();
          count = 1;
        }else {
          count++;
        }
     
      }
     
      @Override
      public void unlock() {
        if (count == 1) {
          count = 0;
          value.compareAndSet(1, 0);
        }else
          count--;
      }
    }

    下面我们通过一个递归程序去验证我们写的可重入的自旋锁是否能够成功工作。

    测试程序:

    import java.util.concurrent.TimeUnit;
     
    public class ReentrantSpinLockTest {
     
      public static int data;
      public static ReentrantSpinLock lock = new ReentrantSpinLock();
     
      public static void add(int state) throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        if (state <= 3) {
          lock.lock();
          System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state);
          for (int i = 0; i < 10; i++)
            data++;
          add(state + 1);
          lock.unlock();
        }
      }
     
      public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
          threads[i] = new Thread(new Thread(() -> {
            try {
              ReentrantSpinLockTest.add(1);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }, String.valueOf(i)));
        }
        for (int i = 0; i < 10; i++) {
          threads[i].start();
        }
        for (int i = 0; i < 10; i++) {
          threads[i].join();
        }
        System.out.println(data);
      }
    }

    上面程序的输出:

    Thread-3    进入临界区 state = 1
    Thread-3    进入临界区 state = 2
    Thread-3    进入临界区 state = 3
    Thread-0    进入临界区 state = 1
    Thread-0    进入临界区 state = 2
    Thread-0    进入临界区 state = 3
    Thread-9    进入临界区 state = 1
    Thread-9    进入临界区 state = 2
    Thread-9    进入临界区 state = 3
    Thread-4    进入临界区 state = 1
    Thread-4    进入临界区 state = 2
    Thread-4    进入临界区 state = 3
    Thread-7    进入临界区 state = 1
    Thread-7    进入临界区 state = 2
    Thread-7    进入临界区 state = 3
    Thread-8    进入临界区 state = 1
    Thread-8    进入临界区 state = 2
    Thread-8    进入临界区 state = 3
    Thread-5    进入临界区 state = 1
    Thread-5    进入临界区 state = 2
    Thread-5    进入临界区 state = 3
    Thread-2    进入临界区 state = 1
    Thread-2    进入临界区 state = 2
    Thread-2    进入临界区 state = 3
    Thread-6    进入临界区 state = 1
    Thread-6    进入临界区 state = 2
    Thread-6    进入临界区 state = 3
    Thread-1    进入临界区 state = 1
    Thread-1    进入临界区 state = 2
    Thread-1    进入临界区 state = 3
    300

    从上面的输出结果我们就可以知道,当一个线程能够获取锁的时候他能够进行重入,而且最终输出的结果也是正确的,因此验证了我们写了可重入自旋锁是有效的!

    本文内容来源于互联网,如有侵权请联系删除。
    作者最新文章
    编程开发
    相关文章 更多
    C++动态数组初始化怎么写?常用语句与代码示例
    C++动态数组初始化怎么写?常用语句与代码示例

    深入解析C++中动态数组的初始化机制,涵盖new操作符的不同用法、基本类型与类对象的初始化差异,以及为何在现代C++开发中应优先使用std::vector。

    谷歌浏览器Mac版入口
    谷歌浏览器Mac版入口

    谷歌浏览器Mac版官方安装指南 谷歌浏览器Mac版官方安装入口是https://www.google.com/chrome/,需macOS 12+系统、500MB空间,下载.dmg后拖入应用程序安装,支持多设备同步、性能优化与隐私保护功能。 苹果电脑Chrome的安装入口究竟在哪里?这个问题最近可是

    Chrome浏览器JS脚本不运行怎么办
    Chrome浏览器JS脚本不运行怎么办

    Chrome中JavaScript未执行需依次检查:一、移除站点级禁用并添加允许域名;二、开启全局JavaScript开关;三、禁用干扰扩展;四、在开发者工具中启用JavaScript;五、重置内容设置为默认。 有时在Chrome里打开网页,会发现交互按钮点了没反应,数据加载不出来,页面仿佛“静止”

    IE浏览器怀旧版在线网址
    IE浏览器怀旧版在线网址

    IE浏览器怀旧版在线网址:一次精准的技术时光回溯 最近,不少老用户和怀旧爱好者在反复搜索一个问题:那个经典的Internet Explorer,如今还能在哪里原汁原味地体验到?答案指向一个特定的地址:https://ie.microsoft.com/legacy/。 这个网站远不止是一个简单的“皮肤

    火狐浏览器有哪些设置功能
    火狐浏览器有哪些设置功能

    火狐浏览器五大核心设置功能:解锁高效、安全与个性化体验 火狐浏览器功能强大,但如果不仔细挖掘,很多能大幅提升效率和安全性的设置可能就“藏着掖着”了。这就好比拥有一台高性能设备,却只用了基础模式。那么,如何把它调整到最顺手、最安全的状态?接下来,我们就聚焦于当前版本(截至2025年末)最关键的五大设置

    chrome搜索免验证入口
    chrome搜索免验证入口

    Chrome官方免验证入口为https://www.google.cn/chrome/,提供全平台安装包、免登录即用、本地化安全机制及引擎级性能优化。 到底该去哪里找正版、免费且无需繁琐验证的Chrome浏览器入口?这个问题困扰了不少网友。今天,我们就来直通核心,为大家详细拆解Chrome引擎的官方

    java heap space 选型思路:使用场景与区别整理
    java heap space 选型思路:使用场景与区别整理

    Java堆是JVM存储对象的核心内存区域,配置需结合场景:单体应用适中设置;大数据处理需大堆并关注GC停顿;微服务强调快速启动;高并发需精细划分堆区域。关键参数-Xms和-Xmx建议等值以稳定性能。垃圾回收器选择影响效率,如G1适用于大堆,ZGC可实现低停顿。内存错误时需监控堆状态。

    java heap space 使用中遇到的问题怎么解决
    java heap space 使用中遇到的问题怎么解决

    Java堆内存溢出错误通常因内存泄漏、数据处理需求过大或JVM参数配置不当引起。排查时可借助jmap、堆转储及MAT等工具定位问题。解决方案包括调整JVM内存参数(如-Xmx)、修复代码中的内存泄漏、优化大数据处理逻辑,并建立持续监控与预防机制,以保障应用稳定运行。

    java xml 选型思路:使用场景与区别整理
    java xml 选型思路:使用场景与区别整理

    XML在Java开发中用于配置、数据交换等场景。解析方式主要有DOM、SAX、StAX及第三方库。DOM适合操作小文件,SAX/StAX适合处理大文件流,JAXB用于对象与XML映射。选型需结合数据大小、内存、性能及团队熟悉度,现代框架常封装底层解析。

    using namespace 使用中遇到的问题怎么解决
    using namespace 使用中遇到的问题怎么解决

    命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    macOS软件
    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    Mac软件 更多
    灵活计算器
    灵活计算器
    macOS/iOS/Android

    灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

    赤友清理大师
    赤友清理大师
    macOS

    赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    极度公式
    极度公式
    Windows/macOS/Linux

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    WINDOWS 更多
    Windows 10
    Windows 10
    Windows

    Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

    极度公式
    极度公式
    Windows/macOS/Linux

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    密码键盘
    密码键盘
    Windows/macOS/iOS/Android

    密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。