线程池的设计原理是什么?

更新时间:2023-06-14 13:22:50 阅读: 评论:0

线程池的设计原理是什么?
导读
线程池相关的知识点是⾯试中⾮常⾼频的问题,掌握线程及线程池相关的知识点也是程序员向⾼段位进阶的必由之路。由于线程池涉及线程、并发、编程语⾔内存模型等多⽅⾯的知识,历来也不是⼀块特别好掌握的内容。因此,⼩码哥决定好好梳理下这⽅⾯的知识,希望能够对你有所帮助。在本⽂中,作者将以JAVA语⾔中的线程池设计为基础,从原理分析及代码实践两个⽅⾯来进⾏梳理。
线程的概念
在了解线程池的相关的知识之前,我们有必要再次深⼊理解下线程的基本概念。在这⾥,也许会有很多同学质疑,线程的基本概念我们都懂,为什么还需要重复提起呢?
在回答这个问题之前,我们还是先回到实际的编程语⾔中来看看线程到底是什么?以JAVA为例,在JAVA中如何实现⼀个线程呢?
public class ThreadDemo01 {
public static void main(String args[]) {
//通过匿名内部类的⽅式创建线程,并且重写其中的run⽅法
new Thread() {
public void run() {
while (true) {
System.out.println("线程->" + Thread.currentThread().getName() + " 运⾏中!");
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
通过上⾯的代码⽰例,我们知道在JAVA中要实现⼀个线程可以通过构造Thread类来实现。之后,通过重写run()⽅法来让线程执⾏我们想要让它执⾏的逻辑。然⽽,为了让线程⽣效,我们还需要通过调⽤start()⽅法来启动它。那么为什么我们重写了run()⽅法,但是却还需要调⽤start()⽅法呢?run()⽅法和start()⽅法有什么关系?到底那个⽅法才是真正代表了线程这个存在呢?
要搞清楚这个问题,需要我们明确“线程的执⾏单元”与“线程”是两个不同的概念。在JAVA中通过Thread类重写的run()⽅法是线程的执⾏单元,⽽通过调⽤start()⽅法才是真正启动了⼀个线程。这⼀点对后⾯我们理解线程池的作⽤会⽐较有⽤,因为只有从概念上剥离线程的执⾏单元与线程本⾝才能更深⼊的理解线程池存在的意义。
为了更加深⼊的说明这⼀点,我们可以来具体分析下上⾯例⼦中start()⽅法在JDK中的源码:
public synchronized void start() {
group.add(this);
boolean started = fal;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
在start()⽅法的源码中,最核⼼的部分其实就是start0()这个JNI本地⽅法:
private native void start0();
也就是说在start⽅法中会调⽤start0这个本地⽅法,但是从源码上这么看⼜看不出start0的具体逻辑。为此,作者特地翻了下JDK的官⽅⽂档,其中关于start⽅法的说明如下:
Caus this thread to begin execution; the Java Virtual Machine calls the run method of this thread.肯迪尼
上⾯这句话的意思是:在开始执⾏这个线程的时候,JVM将会调⽤该线程的run⽅法,⽽实际上run⽅法是被本地⽅法start0()调⽤的。也就是说,在JAVA中由于语⾔的约定,我们需要在使⽤线程时重写线程中的执⾏单元⽅法来实现业务逻辑,⽽真正开启线程资源的则是start ⽅法。手指肌腱断裂
在不少关于JAVA线程的软⽂或者书籍中,经常会提到,创建线程有两种⽅式:第⼀种是构造⼀个Thre月亮船
ad;第⼆种是实现Runnable接⼝。通过上⾯的分析,这种说法其实是不严谨的。在JDK中代表线程的只有Thread类,⽽Runnable接⼝只是简单定义了⼀个⽆参数返回值的run⽅法。⽽我们知道run⽅法只是定义了线程的执⾏单元,⽽并⾮直接开启了线程资源,只有Thread⽅法的start()⽅法才可以启动⼀个线程。
所以,如果⾯试中有⼈问你在JAVA中实现线程的⽅式有哪些?应该告诉他准确答案:“在JAVA中创建线程只有⼀种⽅式,那就是构造Thread类。⽽实现线程的执⾏单元则有两种⽅式,第⼀种是重写Thread类的run⽅法;第⼆种是实现Runnable接⼝的run⽅法,并且将Runnable实例⽤作构造Thread的参数”。
接下来让我们再来回顾下线程的定义:“线程是⼀种轻量级的进程,是由进程派⽣出来的⼦任务,它是程序执⾏的⼀个路径;每个线程都有⾃⼰的局部变量表、程序计数器(指向真正执⾏的指令指针)以及各⾃的⽣命周期”。例如,当启动了⼀个JVM时,从操作系统开始就会创建⼀个新的JVM进程,之后JVM进程中将会派⽣或者创建很多线程。
线程知识涉及编程语⾔特性的⾯⾮常⼴泛,以JAVA语⾔为例,作者梳理了⼀份有关线程的知识图谱,如下:
要掌握JAVA中的线程,需要我们理解线程的⽣命周期、Thread类提供的⽅法细节、线程安全问题等多⽅⾯的知识点。⽽其中线程安全相关的问题⼜涉及JVM的内存模型、线程同步及锁相关的知识。由于篇幅的关系,这⾥作者也只能给出⼀个⼤致的提纲,更细节的内容在后⾯有时间再和⼤家⼀起细化同步。
以上就是在具体讲述线程池之前有关线程知识的回顾了,接下来就让我们进⼊本篇⽂章的主题“线程池”相关的内容吧!
线程池原理
在上节关于线程知识的回顾中,我们知道创建⼀个线程Thread其实是⽐较耗费操作系统资源的,况且系统中可创建的线程数量也是有限的,如果创建的线程资源数量不能够很好的加以限制,反⽽会导致系统性能的下降。因此我们在进⾏多线程编程时,对线程资源的重复利将是⼀种⾮常好的程序设计习惯。店铺介绍怎么写
那么我们在编程时如何才能实现线程资源的重复利⽤呢?答案就是使⽤线程池!所谓的线程池,通俗的理解就是有⼀个池⼦,⾥⾯存放着已经创建好的线程资源,当有任务提交给线程池执⾏时,池中的某个线程就会主动执⾏该任务,执⾏完任务后该线程就会继续回到池⼦中等待下次任务的执⾏。下⾯我们就来看⼀下线程池的基本原理图,如下:
线程池中的线程资源是Thread类代表的,⽽具体的执⾏任务是由实现Runnable接⼝的线程执⾏单元类组成。线程的执⾏单元逻辑随业务的变化⽽有所不同,⽽线程则是⼀个公共资源,所以可以复⽤,这⼀点也是我们在前⾯内容中特别强调的,因为如果我们将线程的执⾏单元中的逻辑与线程本⾝混在⼀起理解的话就很容易产⽣疑惑。
那么如何实现⼀个线程池呢?⼀个完整的线程池应该具备如下要素:
任务队列:⽤于缓存提交的任务。
线程数量管理功能:⼀个线程池必须能够很好地管理和控制线程的数量。⼤致会有三个参数,创建线
程池时的初始线程数量init;⾃动扩充时的最⼤线程数量max;在线程池空闲时需要释放资源但是也要维持⼀定数量的核⼼线程数量core。通过这三个基本参数维持好线程池中数量的合理范围,⼀般来说它们之间的关系是“init<=core<=max”。
任务拒绝策略:如果线程数量已达到上限且任务队列已满,则需要有相应的拒绝策略来通知任务的提交者。
线程⼯⼚:主要⽤于个性化定制线程,如设置线程的名称或者将线程设置为守护线程等。
QueueSize:任务队列主要存放提交的Runnable,但是为了防⽌内存溢出,需要有limit数量对其进⾏限制。
Keepedalive时间:该时间主要决定线程各个重要参数⾃动维护的时间间隔。
通过上⾯对线程池组成部分及原理的分析,为了更加深刻地理解下线程池,下⾯我们⼿⼯实现⼀个线程池!UML类图如下:
ThreadPool(接⼝):主要定义⼀个线程池应该具备的基本操作和⽅法。
RunnableQueue(接⼝):定义存放提交的线程执⾏单元Runnable的队列。
ThreadFactory(接⼝):定义创建线程的接⼝,便于个性化地定制Thread。
DenyPolicy(接⼝):拒绝策略接⼝,主要⽤于Queue中当runnable达到limit上限后所采⽤的拒绝策略。
Internaltask(类):Runnable的实现,⽤于线程池内部,该类通过沦陷RunnableQueue队列,不断从队列中取出任务进⾏执⾏。
LinkedRunnableQueue(类):队列接⼝的具体实现。
BasicThreadPool(类):线程池的核⼼实现类。上海城市规划馆
⼿⼯编写完线程池后,我们看看怎么使⽤:
public class ThreadPoolTest
public static void main(String args[]) throws InterruptedException {
//定义线程池,初始化线程数为2,核⼼线程数为4,最⼤线程数为6,任务队列最多允许1000个任务
final ThreadPool threadPool = new BasicThreadPool(2, 6, 4, 1000);
//定义20个任务并提交给线程池
for (int i = 0; i < 20; i++) {
try {
TimeUnit.SECONDS.sleep(10);
System.out.println(Thread.currentThread().getName() + " is running and done.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
清明手抄报}
}
}
以上测试代码,我们通过初始化2个线程、核⼼线程数为4,最⼤为6,然后向该线程池提交20个任务,执⾏结果如下:
thread-pool-0 is running and done.
陈着
thread-pool--1 is running and done.
thread-pool--2 is running and done.
thread-pool--3 is running and done.
thread-pool-0 is running and done.
thread-pool--1 is running and done.
thread-pool--2 is running and done.
thread-pool--3 is running and done.
thread-pool--1 is running and done.
thread-pool-0 is running and done.
thread-pool--3 is running and done.
thread-pool--2 is running and done.
thread-pool--1 is running and done.
thread-pool-0 is running and done.
thread-pool--3 is running and done.
thread-pool--2 is running and done.
thread-pool-0 is running and done.
thread-pool--1 is running and done.
小小动物园作文四年级
thread-pool--2 is running and done.
thread-pool--3 is running and done.
从运⾏结果看,由于提交速度⽐较快,线程池扩容到了其核⼼线程的数量,总共4个线程,然后这些线程逐步完成了20个任务的执⾏,从⽽实现了线程的重复使⽤。
通过⼿⼯编写线程池的⽬的只是为了让⼤家更好地理解线程池的实现原理,实际上在JDK1.5以后在"urrent(简称JUC)"中已经提供了多种版本的线程池实现,所以在JAVA中使⽤线程池时,我们只需要选择合适的线程池类型即可,⽽这些线程池的实现也基本上与我们⼿⼯编写的线程池原理类似。
Java⾃带线程池
在Java中通过Executor框架提供线程池⽀持,通过该框架我们可以创建出如下⼏类线程池:

本文发布于:2023-06-14 13:22:50,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/89/1038111.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:线程   任务   数量   实现   创建   资源   需要   知识
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图