手写一个简单的线程池

手写一个简单的线程池

项目仓库:https://gitee.com/bossDuy/hand-tearing-thread-pool
基于一个b站up的课程:https://www.bilibili.com/video/BV1cJf2YXEw3/?spm_id_from=333.788.videopod.sections&vd_source=4cda4baec795c32b16ddd661bb9ce865

理解线程池的原理

线程池就是为了减少频繁的创建和销毁线程带来的性能损耗,工作原理:

在这里插入图片描述

简单的说:线程池就是有一个存放线程的集合和一个存放任务的阻塞队列。当提交一个任务的时候,判断核心线程是否满了,没满就会创建一个核心线程加入线程池并且执行任务,核心线程是不会被销毁的即使没有任务执行;满了就会放入任务队列等待;如果队列满了的话就会创建非核心线程进行执行任务,这些非核心线程在不执行任务的时候就会等一段时间销毁(配置的过期时间),如果创建的线程达到了最大线程数,那么就会执行拒绝策略。

可以简要整理如下:

提交任务 -> 核心任务是否已满为满,创建核心线程并执行任务已满,则加入任务队列队列未满 -> 等待执行队列已满 -> 创建非核心线程达到线程最大数量 -> 拒绝策略未达到最大数量 -> 执行任务

自己实现简单的线程池

第一步:实现了一个线程复用的线程池

package com.yb0os1;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;public class MyThreadPool {//1、线程什么时候创建?/**核心线程中我们要保证线程是可以复用的,那么就不可以直接new Thread(task).start(); 这样执行完task线程就会被销毁了我们将接收到的任务对象放到队列中,然后线程从队列中取出任务,通过任务的run方法进行调用,这样就是在该线程上调用任务,并且调用完后不会销毁线程*///2、我们一开始使用 while (true) if(!tasks.isEmpty()) Runnable task = tasks.remove(0);/**这样如果任务队列一直为空就会一循环,消耗cpu资源。此时就是阻塞队列出现了,当为空阻塞等待 非空执行*/BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(1024);Thread thread = new Thread(()->{while (true){if(!taskQueue.isEmpty()){try {Runnable task = taskQueue.take();task.run();} catch (InterruptedException e) {throw new RuntimeException(e);}}}},"唯一线程");{thread.start();//启动线程}public void execute(Runnable task){taskQueue.offer(task);//向队列添加元素 尽量是否offer 满则返回false  add满则排除异常}
}
package com.yb0os1;public class Main {public static void main(String[] args) {MyThreadPool myThreadPool = new MyThreadPool();for (int i = 0; i < 5; i++) {myThreadPool.execute(()->{try {Thread.sleep(1000);} catch (InterruptedException e) {//InterruptedException这个是线程中断异常,// 这个异常一般都是线程在等待或者阻塞中被中断了就会抛出的,// sleep wait等等都是有,除了LockSupport.park 这个会记录中断位 不会抛出这个异常e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"执行完毕");});}System.out.println("主线程没有被阻塞");}
}

测试结果:

在这里插入图片描述

第二步:实现多个线程复用的线程池

package com.yb0os1;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;public class MyThreadPool {//任务队列private final BlockingQueue<Runnable> taskQueue;//核心线程的数量private final int corePoolSize;//最大线程的数量private final int maxPoolSize;private final int keepAliveTime;private final TimeUnit unit;public MyThreadPool(int corePoolSize, int maxPoolSize, int keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> taskQueue) {this.corePoolSize = corePoolSize;this.maxPoolSize = maxPoolSize;this.keepAliveTime = keepAliveTime;this.unit = unit;this.taskQueue = taskQueue;}//核心线程List<Thread> coreList = new ArrayList<>();//非核心线程List<Thread> supportList = new ArrayList<>();//添加元素和判断长度不是原子的,所以存在线程安全问题 可以加锁 CAS等解决public void execute(Runnable command) {//目前线程列表中线程数量小于核心线程的数量,则创建线程if (coreList.size() < corePoolSize) {Thread thread = new CoreThread();coreList.add(thread);thread.start();
//            return;}//成功添加到阻塞队列if (taskQueue.offer(command)) {return;}//任务队列也满了 需要创建非核心线程//核心线程满 任务队列满 但是非核心线程没有满才可以添加if (coreList.size() + supportList.size() < maxPoolSize) {Thread thread = new SupportThread();supportList.add(thread);thread.start();return;}//我们创建完线程之后 并没有处理刚才的command 不能确定是否队列真的满了if (!taskQueue.offer(command)) {//真的满了 抛出异常throw new RuntimeException("线程池已满");}}class CoreThread extends Thread {@Overridepublic void run() {while (true) {try {Runnable task = taskQueue.take();task.run();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}class SupportThread extends Thread {@Overridepublic void run() {while (true) {try {Runnable command  = taskQueue.poll(keepAliveTime, unit);//等待一秒没有获取就会返回nullif (command  == null) {//线程结束break;}command.run();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName()+"非核心线程结束");supportList.remove(Thread.currentThread());System.out.println("当前非核心线程数量为:" + supportList.size());}}
}
package com.yb0os1;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;public class Main {public static void main(String[] args) {MyThreadPool myThreadPool = new MyThreadPool(2,4,1, TimeUnit.SECONDS,new ArrayBlockingQueue<>(2));for (int i = 0; i < 4; i++) {myThreadPool.execute(()->{try {Thread.sleep(1000);} catch (InterruptedException e) {//InterruptedException这个是线程中断异常,// 这个异常一般都是线程在等待或者阻塞中被中断了就会抛出的,// sleep wait等等都是有,除了LockSupport.park 这个会记录中断位 不会抛出这个异常e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"执行完毕");});}System.out.println("主线程没有被阻塞");}
}

存在问题,任务没有被正确的执行:

在这里插入图片描述

b站评论区指出的:if (blockingQueue.offer(command)) { return; } 这里如果任务成功放入队列,方法就直接 return 了。 但在 创建 SupportThread 的逻辑中,没有保证这个任务会被执行,因为 offer() 失败后你才创建新线程。 但 command 并没有交给这个新线程,而是再次尝试 offer(),如果失败就直接走拒绝策略了。 这样的话,可能 SupportThread 已经启动,但任务却没被执行。

理解:如果队列满了,我们创建非核心线程,但是并没有将这任务直接交给我们创建的新线程,而是再次尝试加入队列中,这就导致了一个不确定的状态:

  1. 如果此时队列还是满的(offer 返回 false),就会直接抛出异常,任务未被执行
  2. 如果队列此时恰好有空间(可能因为其他线程刚刚完成了任务,从而腾出了队列空间),那么任务会被放入队列,后续由某个线程(可能是核心线程,也可能是其他非核心线程)从队列中取出并执行。但新创建的非核心线程可能并没有真正处理这个任务。

解决方案:如果队列满了,我们要创建非核心线程并且由这个线程执行任务

也可以说 让线程执行当前 command 之后,再从 queue 中拿任务

第三步:修复bug 设计拒绝策略

package com.yb0os1;import com.yb0os1.reject.DiscardRejectHandle;
import com.yb0os1.reject.ThrowRejectHandle;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;public class Main {public static void main(String[] args) {MyThreadPool myThreadPool = new MyThreadPool(2,4,1, TimeUnit.SECONDS,new ArrayBlockingQueue<>(2),new DiscardRejectHandle());for (int i = 0; i < 8; i++) {int finalI = i;myThreadPool.execute(()->{try {Thread.sleep(100);} catch (InterruptedException e) {//InterruptedException这个是线程中断异常,// 这个异常一般都是线程在等待或者阻塞中被中断了就会抛出的,// sleep wait等等都是有,除了LockSupport.park 这个会记录中断位 不会抛出这个异常e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"执行完毕---"+ finalI);});}System.out.println("主线程没有被阻塞");}
}
package com.yb0os1;import com.yb0os1.reject.RejectHandle;import java.sql.SQLOutput;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;public class MyThreadPool {public BlockingQueue<Runnable> getTaskQueue() {return taskQueue;}//任务队列private final BlockingQueue<Runnable> taskQueue;//核心线程的数量private final int corePoolSize;//最大线程的数量private final int maxPoolSize;private final int keepAliveTime;private final TimeUnit unit;//拒绝策略private final RejectHandle rejectHandle;public MyThreadPool(int corePoolSize, int maxPoolSize, int keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> taskQueue,  RejectHandle rejectHandle) {this.corePoolSize = corePoolSize;this.maxPoolSize = maxPoolSize;this.keepAliveTime = keepAliveTime;this.unit = unit;this.taskQueue = taskQueue;this.rejectHandle = rejectHandle;}//核心线程List<Thread> coreList = new ArrayList<>();//非核心线程List<Thread> supportList = new ArrayList<>();//添加元素和判断长度不是原子的,所以存在线程安全问题 可以加锁 CAS等解决public void execute(Runnable command) {//目前线程列表中线程数量小于核心线程的数量,则创建线程if (coreList.size() < corePoolSize) {Thread thread = new CoreThread(command);coreList.add(thread);thread.start();return;}//成功添加到阻塞队列if (taskQueue.offer(command)) {return;}//任务队列也满了 需要创建非核心线程//核心线程满 任务队列满 但是非核心线程没有满才可以添加if (coreList.size() + supportList.size() < maxPoolSize) {Thread thread = new SupportThread(command);supportList.add(thread);thread.start();return;}//我们创建完线程之后 并没有处理刚才的command 不能确定是否队列真的满了if (!taskQueue.offer(command)) {//真的满 使用拒绝策略rejectHandle.reject(command,this);}}//优先处理传过来的 然后再去阻塞队列中获取class CoreThread extends Thread {private final Runnable command;CoreThread(Runnable command) {this.command = command;}@Overridepublic void run() {command.run();while (true) {try {Runnable task = taskQueue.take();System.out.println("核心线程"+Thread.currentThread().getName()+"正在执行任务");task.run();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}class SupportThread extends Thread {private final Runnable command;SupportThread(Runnable command) {this.command = command;}@Overridepublic void run() {command.run();while (true) {try {Runnable command  = taskQueue.poll(keepAliveTime, unit);//等待一秒没有获取就会返回nullif (command  == null) {//线程结束break;}command.run();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName()+"非核心线程结束");supportList.remove(Thread.currentThread());
//            System.out.println("当前非核心线程数量为:" + supportList.size());}}
}
package com.yb0os1.reject;import com.yb0os1.MyThreadPool;public interface RejectHandle {void reject(Runnable command, MyThreadPool myThreadPool);
}
package com.yb0os1.reject;import com.yb0os1.MyThreadPool;public class DiscardRejectHandle implements RejectHandle{@Overridepublic void reject(Runnable command, MyThreadPool myThreadPool) {myThreadPool.getTaskQueue().poll();System.out.println("任务被丢弃");}
}
package com.yb0os1.reject;import com.yb0os1.MyThreadPool;public class ThrowRejectHandle implements RejectHandle{@Overridepublic void reject(Runnable command, MyThreadPool myThreadPool) {throw new RuntimeException("线程池已满");}
}

思考

在这里插入图片描述

1.你能给线程池增加一个shutdown功能吗

答:关闭线程池分两种情况, 一个是清空任务队列、线程全部完成任务后关闭; 二是等线程完成后直接关,不管队列中的任务。

2、怎么理解拒绝策略

答:首先它是一个策略模式,在线程池的代码中,当任务队列满时就会触发该接口的方法,所以我们只要实现这个接口方法,再把实现类传入线程池即可,并且方法里还可以拿到被拒绝的任务、线程池对象来实现自己的拒绝逻辑。

3、ThreadFactory参数

答:这个参数是线程池用来创建核心、辅助线程的方法,我们可以自定义线程名称等参数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/82041.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

手机打电话时由对方DTMF响应切换多级IVR语音菜单(完结)

手机打电话时由对方DTMF响应切换多级IVR语音菜单&#xff08;完结&#xff09; --本地AI电话机器人 上一篇&#xff1a;手机打电话时由对方DTMF响应切换多级IVR语音菜单&#xff08;话术脚本与实战&#xff09; 下一篇&#xff1a;编写中 一、前言 经过前面几个篇章的详细阐…

Android.mk解析

一、变量说明: 1.LOCAL_PATH:= $(call my-dir) 此行代码在Android.mk的开头,用于给出当前文件的路径 LOCAL_PATH 用于在开发树中查找源文件 宏函数’my-dir’, 由编译系统提供,用于返回当前路径(即包含Android.mk file文件的目录) 2.LOCAL_PACKAGE_NAME := SecSettings …

ip地址改了网络还能用吗?ip地址改了有什么后果

当用户发现自己的网络出现异常时&#xff0c;常常会疑惑&#xff1a;如果IP地址被更改&#xff0c;网络是否还能正常使用&#xff1f;要解答这个问题&#xff0c;需要从IP地址的作用、修改方式以及网络配置等多个角度来分析。 一、IP地址的作用 IP地址是设备在网络中的唯一标识…

Python-Django系列—日志

Python 程序员通常会在其代码中使用 print() 作为一种快速和方便的调试工具。使用日志框架只比这多花一点点工夫&#xff0c;但更加优雅和灵活。除了用于调试之外&#xff0c;日志还可以为您提供有关应用程序状态和健康状况的更多信息&#xff0c;而且这些信息结构更清晰。 一…

ArcGIS Pro对图斑进行等比例、等面积、等宽度的分割

ArcGIS全系列实战视频教程——9个单一课程组合系列直播回放_arcgis视频教程我要自学网-CSDN博客 4大遥感软件&#xff01;遥感影像解译&#xff01;ArcGISENVIErdaseCognition_遥感解译软件-CSDN博客 今天介绍一下ArcGIS Pro对图斑进行等比例、等面积、等宽度的分割&#xff0…

”故茗”茶文化网站

摘 要 计算机网络发展到现在已经好几十年了&#xff0c;在理论上面已经有了很丰富的基础&#xff0c;并且在现实生活中也到处都在使用&#xff0c;可以说&#xff0c;经过几十年的发展&#xff0c;互联网技术已经把地域信息的隔阂给消除了&#xff0c;让整个世界都可以即时通话…

【和春笋一起学C++】(十五)字符串作为函数参数

1. char指针作为函数参数 在C语言中&#xff0c;表示字符串的方式有3种&#xff1a; char数组用引号括起的字符串常量char指针 这3种形式都可以将其作为实参传递给函数中的参数&#xff08;char*&#xff09;&#xff0c;因此函数的形参需要使用char*类型。将字符串作为参数…

VueRouter路由组件的用法介绍

1.1、<router-link>标签 <router-link>标签的作用是实现路由之间的跳转功能&#xff0c;默认情况下&#xff0c;<router-link>标签是采用超链接<a>标签显示的&#xff0c;通过to属性指定需要跳转的路由地址。当然&#xff0c;如果你不想使用默认的<…

【C/C++】胜者树与败者树:多路归并排序的利器

文章目录 胜者树与败者树&#xff1a;多路归并排序的利器1 胜者树简介1.1 定义1.2 胜者树结构与原理1.2.1 构造流程1.2.2 归并过程 2 败者树简介2.1 背景场景2.2 基本定义2.3 败者树结构和原理2.3.1 树的构造&#xff08;初始建树&#xff09;2.3.2 查询和更新 3 胜者树 vs 败者…

零基础设计模式——第二部分:创建型模式 - 原型模式

第二部分&#xff1a;创建型模式 - 5. 原型模式 (Prototype Pattern) 我们已经探讨了单例、工厂方法、抽象工厂和生成器模式。现在&#xff0c;我们来看创建型模式的最后一个主要成员——原型模式。这种模式关注的是通过复制现有对象来创建新对象&#xff0c;而不是通过传统的…

C++(初阶)(十九)——红黑树

红黑树 红黑树概念规则实现结点插入变色变色参考代码&#xff1a; 查找查找参考代码 遍历 红黑树检查完整代码 概念 红⿊树是⼀棵⼆叉搜索树。它的每个结点增加⼀个存储位来表示结点的颜⾊&#xff0c;可以是红色或者黑色&#xff08;并不会出现第三种颜色&#xff09;。 通过…

Mistral AI 开源最新 Small 模型——Devstral-Small-2505

Devstral 是一款专为软件工程任务设计的代理型大语言模型&#xff08;LLM&#xff09;&#xff0c;由 Mistral AI 和 All Hands AI 合作开发 &#x1f64c;。Devstral 擅长使用工具探索代码库、编辑多个文件以及驱动软件工程代理。该模型在 SWE-bench 上表现出色&#xff0c;使…

CDGA|一线二线企业数据治理项目目前发展状况

一线城市与二线城市企业在数据治理项目的发展状况上存在一定差异&#xff0c;主要体现在目标、资源投入、策略实施以及文化培育等方面。 一线城市企业数据治理项目发展状况 ‌数据治理目标全面系统‌&#xff1a; ‌数据质量与安全‌&#xff1a;一线城市的大型企业通常拥有海量…

Lyra学习笔记1地图角色加载流程

目录 1 地图加载流程1.1 默认Experience的加载1.2 加载角色1.3 加载场景中的几个传送点 2 几个内建类的笔记2.1 UDataAsset2.2 UAssetManager 纯个人笔记&#xff0c;有错误欢迎指正&#xff0c;学习阶段基本看到不会的就写一写&#xff0c;最后有时间会梳理整体结构 先看完了官…

SurfaceFlinger及Android应用RenderThread角度观察Jank丢帧卡顿

SurfaceFlinger及Android应用RenderThread角度观察Jank丢帧卡顿 CPU、GPU、Display 三个部分&#xff1a;CPU 负责计算帧数据&#xff0c;把计算好的数据交给 GPU&#xff0c;GPU 会对图形数据进行渲染&#xff0c;渲染好后放到 buffer &#xff08;图像缓冲区&#xff09;存起…

《牛客》数组中出现次数超过一半的数字

牛客的刷题之路不停歇 ⌓‿⌓ 不积跬步无以至千里&#xff0c;不积小流无以成江海 The harder you work,the luckier you will be 题目及示例 题目链接 描述 给一个长度为 n 的数组&#xff0c;数组中有一个数字出现的次数超过数组长度的一半&#xff0c;请找出这个数字。 例…

七彩喜康养护理——科技赋能下的全周期健康守护

在当今社会&#xff0c;随着人们健康意识的不断提高&#xff0c;护理行业逐渐走向专业化、精细化&#xff0c;而七彩喜智养护理作为一种新兴的护理方式&#xff0c;逐渐受到了广泛的关注和应用。 它不仅仅是针对单一病症的治疗护理&#xff0c;而是一种全面的、全方位的健康管…

【爬虫】12306自动化购票

上文&#xff1a; 【爬虫】12306查票-CSDN博客 下面是简单的自动化进行抢票&#xff0c;只写到预定票&#xff0c;没有写完登陆&#xff0c; 跳出登陆后与上述代码同理修改即可。 感觉xpath最简单&#xff0c;复制粘贴&#xff1a; 还有很多写法&#xff1a; 官网地址&#…

Java设计模式之组合模式:从入门到精通(保姆级教程)

文章目录 1. 组合模式概述1.1 专业定义1.2 通俗解释1.3 模式结构2. 组合模式详细解析2.1 模式优缺点2.2 适用场景3. 组合模式实现详解3.1 基础实现3.2 代码解析4. 组合模式进阶应用4.1 透明式 vs 安全式组合模式4.2 组合模式与递归4.3 组合模式与迭代器5. 组合模式在实际开发中…

游戏如何应对反编译工具dnspy

Unity Mono 是 Unity 引擎默认的脚本运行时环境&#xff0c;由跨平台的开源 .NET 框架实现&#xff0c;它允许开发者使用 C# 等编程语言编写游戏逻辑&#xff0c;凭借简单易用的开发环境和高效的脚本编译速度&#xff0c;得到了众多游戏的青睐。 在 Mono 模式下&#xff0c;游…