当前位置:

首页 > 编程开发 > 怎么手写Java LockSupport

怎么手写Java LockSupport

前言在JDK当中给我们提供的各种并发工具当中,比如ReentrantLock等等工具的内部实现,经常会使用到一个工具,这个工具就是LockSupport。LockSupport给我们提供了一个非常强大的功能,它是线程阻塞最基本的元语,他可以将一个线程阻塞也可以将一个线程唤醒,因此经常在并发的场景下进行使用。LockSupport实现原理在了解LockSupport实现原理之前我们先用一个案例来了解一下LockSupport的功能!importjava.util.concurrent.TimeUnit;im

本文目录

      前言

      在JDK当中给我们提供的各种并发工具当中,比如ReentrantLock等等工具的内部实现,经常会使用到一个工具,这个工具就是LockSupport。LockSupport给我们提供了一个非常强大的功能,它是线程阻塞最基本的元语,他可以将一个线程阻塞也可以将一个线程唤醒,因此经常在并发的场景下进行使用。

      LockSupport实现原理

      在了解LockSupport实现原理之前我们先用一个案例来了解一下LockSupport的功能!

      import java.util.concurrent.TimeUnit;
      import java.util.concurrent.locks.LockSupport;
       
      public class Demo {
       
        public static void main(String[] args) throws InterruptedException {
          Thread thread = new Thread(() -> {
            System.out.println("park 之前");
            LockSupport.park(); // park 函数可以将调用这个方法的线程挂起
            System.out.println("park 之后");
          });
          thread.start();
          TimeUnit.SECONDS.sleep(5);
          System.out.println("主线程休息了 5s");
          System.out.println("主线程 unpark thread");
          LockSupport.unpark(thread); // 主线程将线程 thread 唤醒 唤醒之后线程 thread 才可以继续执行
        }
      }

      上面的代码的输出如下:

      park 之前
      主线程休息了 5s
      主线程 unpark thread
      park 之后

      乍一看上面的LockSupport的park和unpark实现的功能和await和signal实现的功能好像是一样的,但是其实不然,我们来看下面的代码:

      import java.util.concurrent.TimeUnit;
      import java.util.concurrent.locks.LockSupport;
       
      public class Demo02 {
        public static void main(String[] args) throws InterruptedException {
          Thread thread = new Thread(() -> {
            try {
              TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            System.out.println("park 之前");
            LockSupport.park(); // 线程 thread 后进行 park 操作 
            System.out.println("park 之后");
          });
          thread.start();
          System.out.println("主线程 unpark thread");
          LockSupport.unpark(thread); // 先进行 unpark 操作
       
        }
      }

      上面代码输出结果如下:

      主线程 unpark thread
      park 之前
      park 之后

      在上面的代码当中主线程会先进行unpark操作,然后线程thread才进行park操作,这种情况下程序也可以正常执行。但是如果是signal的调用在await调用之前的话,程序则不会执行完成,比如下面的代码:

      import java.util.concurrent.TimeUnit;
      import java.util.concurrent.locks.Condition;
      import java.util.concurrent.locks.ReentrantLock;
       
      public class Demo03 {
       
        private static final ReentrantLock lock = new ReentrantLock();
        private static final Condition condition = lock.newCondition();
       
        public static void thread() throws InterruptedException {
          lock.lock();
       
          try {
            TimeUnit.SECONDS.sleep(5);
            condition.await();
            System.out.println("等待完成");
          }finally {
            lock.unlock();
          }
        }
       
        public static void mainThread() {
          lock.lock();
          try {
            System.out.println("发送信号");
            condition.signal();
          }finally {
            lock.unlock();
            System.out.println("主线程解锁完成");
          }
        }
       
        public static void main(String[] args) {
          Thread thread = new Thread(() -> {
            try {
              thread();
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          });
          thread.start();
       
          mainThread();
        }
      }

      上面的代码输出如下:

      发送信号
      主线程解锁完成

      在上面的代码当中“等待完成“始终是不会被打印出来的,这是因为signal函数的调用在await之前,signal函数只会对在它之前执行的await函数有效果,对在其后面调用的await是不会产生影响的。

      那是什么原因导致的这个效果呢?

      其实JVM在实现LockSupport的时候,内部会给每一个线程维护一个计数器变量_counter,这个变量是表示的含义是“许可证的数量”,只有当有许可证的时候线程才可以执行,同时许可证最大的数量只能为1。当调用一次park的时候许可证的数量会减一。当调用一次unpark的时候计数器就会加一,但是计数器的值不能超过1。

      当一个线程调用park之后,他就需要等待一个许可证,只有拿到许可证之后这个线程才能够继续执行,或者在park之前已经获得一个了一个许可证,那么它就不需要阻塞,直接可以执行。

      自己动手实现自己的LockSupport

      实现原理

      在前文当中我们已经介绍了locksupport的原理,它主要的内部实现就是通过许可证实现的:

      • 每一个线程能够获取的许可证的最大数目就是1。

      • 当调用unpark方法时,线程可以获取一个许可证,许可证数量的上限是1,如果已经有一个许可证了,那么许可证就不能累加。

      • 当调用park方法的时候,如果调用park方法的线程没有许可证的话,则需要将这个线程挂起,直到有其他线程调用unpark方法,给这个线程发放一个许可证,线程才能够继续执行。但是如果线程已经有了一个许可证,那么线程将不会阻塞可以直接执行。

      自己实现LockSupport协议规定

      在我们自己实现的Parker当中我们也可以给每个线程一个计数器,记录线程的许可证的数目,当许可证的数目大于等于0的时候,线程可以执行,反之线程需要被阻塞,协议具体规则如下:

      • 初始线程的许可证的数目为0。

      • 如果我们在调用park的时候,计数器的值等于1,计数器的值变为0,则线程可以继续执行。

      • 如果我们在调用park的时候,计数器的值等于0,则线程不可以继续执行,需要将线程挂起,且将计数器的值设置为-1。

      • 如果我们在调用unpark的时候,被unpark的线程的计数器的值等于0,则需要将计数器的值变为1。

      • 如果我们在调用unpark的时候,被unpark的线程的计数器的值等于1,则不需要改变计数器的值,因为计数器的最大值就是1。

      • 我们在调用unpark的时候,如果计数器的值等于-1,说明线程已经被挂起了,则需要将线程唤醒,同时需要将计数器的值设置为0。

      工具

      因为涉及线程的阻塞和唤醒,我们可以使用可重入锁ReentrantLock和条件变量Condition,因此需要熟悉这两个工具的使用。

      ReentrantLock 主要用于加锁和开锁,用于保护临界区。

      Condition.awat 方法用于将线程阻塞。

      Condition.signal 方法用于将线程唤醒。

      因为我们在unpark方法当中需要传入具体的线程,将这个线程发放许可证,同时唤醒这个线程,因为是需要针对特定的线程进行唤醒,而condition唤醒的线程是不确定的,因此我们需要为每一个线程维护一个计数器和条件变量,这样每个条件变量只与一个线程相关,唤醒的肯定就是一个特定的线程。我们可以使用HashMap进行实现,键为线程,值为计数器或者条件变量。

      具体实现

      因此综合上面的分析我们的类变量如下:

      private final ReentrantLock lock; // 用于保护临界去
      private final HashMap permits; // 许可证的数量
      private final HashMap conditions; // 用于唤醒和阻塞线程的条件变量

      构造函数主要对变量进行赋值:

      public Parker() {
        lock = new ReentrantLock();
        permits = new HashMap<>();
        conditions = new HashMap<>();
      }

      park方法

      public void park() {
        Thread t = Thread.currentThread(); // 首先得到当前正在执行的线程
        if (conditions.get(t) == null) { // 如果还没有线程对应的condition的话就进行创建
          conditions.put(t, lock.newCondition());
        }
        lock.lock();
        try {
          // 如果许可证变量还没有创建 或者许可证等于0 说明没有许可证了 线程需要被挂起
          if (permits.get(t) == null || permits.get(t) == 0) {
            permits.put(t, -1); // 同时许可证的数目应该设置为-1
            conditions.get(t).await();
          }else if (permits.get(t) > 0) {
            permits.put(t, 0); // 如果许可证的数目大于0 也就是为1 说明线程已经有了许可证因此可以直接被放行 但是需要消耗一个许可证
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          lock.unlock();
        }
      }

      unpark方法

      public void unpark(Thread thread) {
        Thread t = thread; // 给线程 thread 发放一个许可证
        lock.lock();
        try {
          if (permits.get(t) == null) // 如果还没有创建许可证变量 说明线程当前的许可证数量等于初始数量也就是0 因此方法许可证之后 许可证的数量为 1
            permits.put(t, 1);
          else if (permits.get(t) == -1) { // 如果许可证数量为-1,则说明肯定线程 thread 调用了park方法,而且线程 thread已经被挂起了 因此在 unpark 函数当中不急需要将许可证数量这是为0 同时还需要将线程唤醒
            permits.put(t, 0);
            conditions.get(t).signal();
          }else if (permits.get(t) == 0) { // 如果许可证数量为0 说明线程正在执行 因此许可证数量加一
            permits.put(t, 1);
          } // 除此之外就是许可证为1的情况了 在这种情况下是不需要进行操作的 因为许可证最大的数量就是1
        }finally {
          lock.unlock();
        }
      }

      完整代码

      import java.util.HashMap;
      import java.util.concurrent.locks.Condition;
      import java.util.concurrent.locks.ReentrantLock;
       
      public class Parker {
       
        private final ReentrantLock lock;
        private final HashMap permits;
        private final HashMap conditions;
       
        public Parker() {
          lock = new ReentrantLock();
          permits = new HashMap<>();
          conditions = new HashMap<>();
        }
       
        public void park() {
          Thread t = Thread.currentThread();
          if (conditions.get(t) == null) {
            conditions.put(t, lock.newCondition());
          }
          lock.lock();
          try {
            if (permits.get(t) == null || permits.get(t) == 0) {
              permits.put(t, -1);
              conditions.get(t).await();
            }else if (permits.get(t) > 0) {
              permits.put(t, 0);
            }
          } catch (InterruptedException e) {
            e.printStackTrace();
          } finally {
            lock.unlock();
          }
        }
       
        public void unpark(Thread thread) {
          Thread t = thread;
          lock.lock();
          try {
            if (permits.get(t) == null)
              permits.put(t, 1);
            else if (permits.get(t) == -1) {
              permits.put(t, 0);
              conditions.get(t).signal();
            }else if (permits.get(t) == 0) {
              permits.put(t, 1);
            }
          }finally {
            lock.unlock();
          }
        }
      }

      JVM实现一瞥

      其实在JVM底层对于park和unpark的实现也是基于锁和条件变量的,只不过是用更加底层的操作系统和libc(linux操作系统)提供的API进行实现的。虽然API不一样,但是原理是相仿的,思想也相似。

      比如下面的就是JVM实现的unpark方法:

      void Parker::unpark() {
        int s, status;
        // 进行加锁操作 相当于 可重入锁的 lock.lock()
        status = pthread_mutex_lock(_mutex);
        assert (status == 0, "invariant");
        s = _counter;
        _counter = 1;
        if (s < 1) {
          // 如果许可证小于 1 进行下面的操作
          if (WorkAroundNPTLTimedWaitHang) {
            // 这行代码相当于 condition.signal() 唤醒线程
            status = pthread_cond_signal (_cond);
            assert (status == 0, "invariant");
            // 解锁操作 相当于可重入锁的 lock.unlock()
            status = pthread_mutex_unlock(_mutex);
            assert (status == 0, "invariant");
          } else {
            status = pthread_mutex_unlock(_mutex);
            assert (status == 0, "invariant");
            status = pthread_cond_signal (_cond);
            assert (status == 0, "invariant");
          }
        } else {
          // 如果有许可证 也就是 s == 1 那么不许要将线程挂起
          // 解锁操作 相当于可重入锁的 lock.unlock()
          pthread_mutex_unlock(_mutex);
          assert (status == 0, "invariant");
        }
      }

      JVM实现的park方法,如果没有许可证也是会将线程挂起的:

      怎么手写Java LockSupport

      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系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 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。