成都网站建设qghl,网页制作,北京奕杰企业管理咨询有限公司,如何购买网站主机目录
前言
一、什么是线程池
1.引入线程池的原因
2.线程池的介绍
二、标准库中的线程池
1.构造方法
2.方法参数
#xff08;1#xff09;corePoolSize 与 maximumPoolSize
#xff08;2#xff09;keepAliveTime 与 unit
#xff08;3#xff09;workQueue1corePoolSize 与 maximumPoolSize
2keepAliveTime 与 unit
3workQueue任务队列
4threadFactory线程工厂
5 handler拒绝策略
3.使用标准库中线程池
三、实现线程池
·结尾 ·前言 在我们学习编程知识过程中一定听说过很多池比如常量池还有在我前面 MySql 专栏中 JDBC 编程里提到的数据库连接池以及本篇文章要为大家介绍的线程池所谓的这些池作用其实都差不多都是提前把要用的对象创建好然后把用完的对象不立即释放留着以备下次使用这样就可以起到提高效率的作用本篇文章就会为大家介绍一下什么是线程池在我们 Java 标准库中的线程池是什么样的以及使用 Java 代码来实现一个简单的线程池让大家能更清晰的认识线程池那么就开始本篇文章的介绍内容吧。
一、什么是线程池
1.引入线程池的原因 在我们最开始引入进程的概念就能够解决并发编程的问题后来由于频繁创建销毁进程带来的开销太大从而引入了线程轻量级进程这样的概念使用复用资源的方式来提高创建销毁的效率但是如果创建和销毁线程的频率也进一步提高呢此时线程的创建和开销也就不能无视了。 为了优化线程的创建与销毁的效率有下面两种解决方案 引入轻量级线程也称为“协程”使用线程池。 为什么协程可以优化线程的开销与销毁这是因为协程的本质是我们在用户态代码中进行调度不是靠内核的调度器调度的这样就可以节省很多调度上的开销此时我们代码中创建上千个线程会卡死但是创建上千个协程就没什么事了。 虽然协程有很多的好处但是在 Java 中不是很推荐用上述做法来优化线程的创建与销毁这是因为引入协程会引入额外的复杂性使用协程可能不是很稳定协程的调试比较困难……所以相比于协程使用线程池对于优化线程的创建与销毁会更好一些那么下面就进一步介绍一下线程池是什么吧。
2.线程池的介绍 线程池就是要把使用的线程提前创建好用完了一个线程也不要直接释放而是放到线程池中以备下次的使用这样就节省了线程创建与销毁的开销因为在这使用线程的过程中并没有真的频繁创建和销毁线程只是从线程池中取线程使用用完还会放回去。 那么为什么从线程池中取线程就比从系统中申请更高效呢这就好比你让室友帮你取快递室友答应帮你取但是什么时候给你取回来他在帮你取快递的途中会不会做一些什么事情都是不确定的相比之下你自己去取快递就会更高效通过上面的例子我们可以得到以下结论 从线程池中取线程是纯用户态代码是可控的通过系统申请创建线程需要系统内核来完成这是不可控的。 二、标准库中的线程池
1.构造方法 在 Java 标准库中 ThreadPoolExecutor 这个类就是用来创建线程池的关于这个类它的构造方法有很多的参数由我来给大家介绍一下下面是这个类的几个构造方法如下图所示 如上图ThreadPoolExecutor 一共涉及到四个构造方法这里我只对第四个构造方法的每个参数进行一个介绍这是因为最后一个构造方法的参数是最全的可以这么理解介绍完第四个构造方法的各个参数其余三个构造方法也就都包含了。
2.方法参数
1corePoolSize 与 maximumPoolSize 在标准库提供的线程池中持有的线程个数并不是一成不变的它会根据当前的任务量来自适应当前线程的个数任务数量很多就会多创建几个线程任务量比较少就会少创建几个线程在构造方法中的前两个参数 int corePoolSize 代表线程池中核心线程数有多少也就是一个线程池中最少得有多少个线程int maximumPoolSize 代表了线程池中最大线程数是多少也就是一个线程池中最多能有多少个线程。
2keepAliveTime 与 unit 第三个参数long keepAliveTime 代表的意思就是线程池中除了核心线程外的线程的保持存活时间在上面介绍了标准库中的线程池是根据当前的任务量来自适应当前线程的个数这个参数就是自适应实现的一个重要标准keepAliveTime 可以记录除了核心线程外的线程空闲的时间如果这些线程的空闲时间超过了 keepAliveTime 的值就会自动销毁这些线程来达到一个自适应的效果这里的第四个参数TimeUnit unit 就是搭配 keepAliveTime 这个参数的unit 代表的是时间单位它可以是 s、min、ms、hour……也就代表了空闲时间 keepAliveTime 的时间单位。
3workQueue任务队列 第五个参数BlockingQueue了Runnable workQueue 代表线程池中可以有很多个任务这里使用 Runnable 来作为描述任务的主体线程池中线程不断从这个阻塞队列中取任务来执行。
4threadFactory线程工厂 第六个参数ThreadFactory threadFactory 意思是线程工厂通过这个工厂类就可以来创建线程对象Thread 对象这个类里提供了方法方法中封装了 new Thread 这样的操作并且同时给 Thread 设置了一些属性由此就构成了 ThreadFactory 线程工厂。 这里的线程工厂也用到了一种设计模式工厂模式它是通过专门的“工厂类”/“工厂对象”来创建指定对象那么为什么使用工厂模式呢我们来看下面的一个代码示例
// 表示平面上的一个点
class Point {// 笛卡尔坐标系 x 与 yprivate double x;private double y;// 极坐标系 r 与 aprivate double r;private double a;// 通过笛卡尔坐标系来构造这个点public Point(double x, double y) {setX(x);setY(y);}// 通过极坐标系来构造这个点public Point(double r, double a) {setR(r);setA(a);}public double x() {return x;}public void setX(double x) {this.x x;}public double y() {return y;}public void setY(double y) {this.y y;}public double r() {return r;}public void setR(double r) {this.r r;}public double a() {return a;}public void setA(double a) {this.a a;}
} 不知道大家看完上面的代码有没有发现什么问题这里的问题就在于 Point 这个类的两个构造方法不构成重载它们的参数列表是一样的如下图所示 想必我们都知道使用笛卡尔坐标和使用极坐标都可以表示一个点并且这两个表示方法并不相同想通过同一个类的构造方法来用这两种不同的方式表示不同的点就违背了 Java 的语法规则为了解决上述的问题就引入了“工厂模式”。 工厂模式的基本逻辑就是使用普通方法来创建对象在普通方法中把构造方法进行封装利用工厂模式修改后的代码及运行结果如下所示
// 表示平面上的一个点
class Point {// 笛卡尔坐标系 x 与 yprivate double x;private double y;// 极坐标系 r 与 aprivate double r;private double a;// 通过笛卡尔坐标系来构造这个点public static Point makePointByXY(double x, double y) {Point point new Point();point.setX(x);point.setY(y);return point;}// 通过极坐标系来构造这个点public static Point makePointByRA(double r, double a) {Point point new Point();point.setR(r);point.setA(a);return point;}public double x() {return x;}public void setX(double x) {this.x x;}public double y() {return y;}public void setY(double y) {this.y y;}public double r() {return r;}public void setR(double r) {this.r r;}public double a() {return a;}public void setA(double a) {this.a a;}
}
public class PointFactory {public static void main(String[] args) {Point point1 Point.makePointByXY(3,4);Point point2 Point.makePointByRA(3,30);System.out.println(point1 的笛卡尔坐标:-( point1.x() , point1.y() ));System.out.println(point2 的极坐标:-( point2.r() , point2.a() ));}
} 此时利用工厂模式就可以创建出两个方式表示的点代码中的 makePointByXY 方法与 makePointByRA 方法也称为工厂方法如果把工厂方法放到一个其他的类中这个类就叫做“工厂类”总的来说通过静态方法封装 new 操作在方法内部设定不同的属性来完成对象的初始化构造对象的过程就是工厂模式。
5 handler拒绝策略 第七个参数RejectedExecutionHandler handler 这个参数可以算是最重要的一个参数在前面介绍的第五个参数BlockingQueue了Runnable workQueue 这是线程池中的一个阻塞队列用来存储当前线程池要执行的任务都有哪些它能够容纳的元素是有上限的此时当这个阻塞队列中的任务已经排满了还有新的任务要往这个阻塞队列中添加线程池该怎么办这就需要我们的第七个参数RejectedExecutionHandler handler 来指明一个拒绝策略如下图所示 上图中的这四个类也就代表了四种拒绝策略它们所对应的拒绝策略如下表所示 拒绝策略 ThreadPoolExecutor.AbortPolicy 继续添加任务直接抛出异常。ThreadPoolExecutor.CallerRunsPolicy新的任务由添加任务的线程负责执行。ThreadPoolExecutor.DiscardOldestPolicy丢弃最老的任务添加新的任务。ThreadPoolExecutor.DiscardPolicy丢弃最新的任务。
3.使用标准库中线程池 上面介绍了 ThreadPoolExecutor 类的构造方法及构造方法中的参数可以看出来 ThreadPoolExecutor 类本身用起来比较复杂因此在标准库中还提供了另一个版本的线程池也就是把 ThreadPoolExecutor 类给封装了一下这个线程池就是 Executors 工厂类通过这个类来创建出的不同线程池对象Executors 类在内部把 ThreadPoolExecutor 创建好了并且设置了不同的参数下面就使用 Executors 演示一下标准库中线程池的效果吧。 如下图所示在 Executors 中内置了很多版本的线程池这里我们使用固定数目的线程池来简单演示一下线程池的效果即可。 下面使用线程池的具体代码及运行结果如下所示
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TestDemo7 {public static void main(String[] args) {// ExecutorService 提供了一种管理和控制异步任务执行的方式ExecutorService service Executors.newFixedThreadPool(4);// 使用 submit 方法把任务添加到线程池中service.submit(new Runnable() {Overridepublic void run() {System.out.println(hello);}});}
} 介绍完这两个标准库中的线程池可以明确一点当我们只是想简单用一下线程池就可以使用 Executors 当我们希望高度定制化一个线程池就可以使用 ThreadPoolExecutor。
三、实现线程池 在前面介绍了标准库中的线程池及演示了使用的效果下面我就来写代码实现一个简单的线程池这里我就直接写一个固定线程数目的线程池下面是这个简单线程池中包含的内容 提供构造方法指定创建多少个线程在构造方法中把这些线程都创建好创建一个阻塞队列能够持有要执行的任务提供 submit 方法可以添加新的任务。 那么下面我就直接上代码了关于这个简单线程池实现的细节我会在代码中以注释的方式进行介绍线程池实现的代码及运行结果如下所示
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class MyThreadPoolExecutor {// 创建阻塞队列,用来接收任务,这里设置最多容纳任务量为 100private BlockingQueueRunnable blockingQueue new ArrayBlockingQueue(100);// 创建线程链表,把创建的每个线程都用线程链表组织起来private ListThread threadList new ArrayList();// 构造方法,指定线程池中固定的线程数,并且将线程都创建好public MyThreadPoolExecutor(int num) {for (int i 0; i num; i) {Thread t new Thread(()-{while (true) {// 利用 runnable 来接收阻塞队列中的任务Runnable runnable null;try {// 获取任务runnable blockingQueue.take();} catch (InterruptedException e) {throw new RuntimeException(e);}// 执行任务runnable.run();}});// 启动线程t.start();// 将线程加入到线程链表中threadList.add(t);}}// 方法 sumbit 用来向阻塞队列中添加新的任务public void sumbit(Runnable runnable) throws InterruptedException {blockingQueue.put(runnable);}
}public class ThreadDemo8 {public static void main(String[] args) throws InterruptedException {// 创建线程池,指定线程数目为 4MyThreadPoolExecutor executor new MyThreadPoolExecutor(4);// 循环 100 次,向线程池中添加 100 个任务for (int i 0; i 100; i) {int n i;executor.sumbit(new Runnable() {Overridepublic void run() {// 任务的内容System.out.println(执行任务:- n ,执行的线程是:- Thread.currentThread().getName());}});}}
} 如上图的运行结果可以看出多个线程之间的执行顺序是不确定的某个线程获取到了某个任务但并非是立即执行在这个过程中很有可能另一个线程就插到前面了这里的这些线程彼此之间都是等价的。
·结尾 文章到这里就要结束了回顾本篇文章我介绍了什么是线程池标准库中线程池还有实现了一个简单的线程池其中还是要多理解一下标准库中线程池构造方法每个参数的意思及理解拒绝策略的含义这可以让我们对 ThreadPoolExecutor 类的使用更加清晰后面实现的线程池也就可以看出线程池基本的工作原理那就是不断利用这 4 个线程来执行任务这样就省去创建和销毁线程的开销那么如果你感觉本篇文章对你有所帮助还是希望能收到你的三连鼓励如果对文章的内容有所疑问欢迎在评论区进行讨论我们下一篇文章再见吧~~~