当前位置:

首页 > 编程开发 > 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

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

      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
      作者最新文章
      编程开发 Java
      相关文章 更多
      Java单元测试入门:使用JUnit 5编写第一个测试用例
      Java单元测试入门:使用JUnit 5编写第一个测试用例

      面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

      Java变量未初始化报错解析:局部变量与成员变量默认值差异
      Java变量未初始化报错解析:局部变量与成员变量默认值差异

      深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

      详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

      Java中final与static final常量的定义及初始化规则
      Java中final与static final常量的定义及初始化规则

      深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

      Java抽象方法与接口方法的区别及选型建议
      Java抽象方法与接口方法的区别及选型建议

      深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

      Java实现HTTP请求与JSON解析的完整代码示例
      Java实现HTTP请求与JSON解析的完整代码示例

      本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

      Java递归出现StackOverflowError的原因分析及栈空间优化方案
      Java递归出现StackOverflowError的原因分析及栈空间优化方案

      详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

      Java空指针异常(NPE)的代码排查与修复实例
      Java空指针异常(NPE)的代码排查与修复实例

      针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

      selenium上传文件教程本地文件路径设置方法
      selenium上传文件教程本地文件路径设置方法

      深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

      针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

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

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

      Windows
      Windows

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

      PDF教程
      PDF教程

      正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

      Mac软件 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      Aperty macOS版
      Aperty macOS版
      macOS

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      WINDOWS 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      PixTV 网页版
      PixTV 网页版
      Windows

      PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。