目录
前言
什么是线程池
线程池的优点
ThreadPollExecutor中的构造方法
corePoolSize && maximumPoolSize
keepAliveTime && unit
workQueue
threadFactory
如何在java中使用线程池
1.创建线程池对象
2.调用submit添加任务
3.调用shutdown关闭线程池
手动实现线程池
创建一个线程池类
测试
前言
在前面我们都是通过new Thread() 来创建线程的,虽然在java中对线程的创建、中断、销毁、等值等功能提供了支持,但从操作系统角度来看,频繁的创建和销毁线程,是需要大量的时间和资源的。那么我们能不能先把线程提前从系统中申请好,需要使用线程的时候,直接从这个地方取出来,而不是从系统中重新申请,等线程用完之后,也是放回到这个地方,而不用频繁的进行创建和销毁呢?那我们就需要用到线程池,线程池能够提高程序的效率。
什么是线程池
线程池 (ThreadPool) 是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建后保持活动的线程中执行这些任务。线程池的主要目的是减少创建和销毁线程的开销,提高响应速度和整体性能。
为什么说线程池里取线程,比从系统中创建一个线程效率要高呢?
我们从线程池里直接取出线程是纯用户态操作,而我们新建一个线程是内核态+用户态配合完成的。
内核态:指操作系统内核及其相关组件(如设备驱动程序)运行的状态。
用户态:指用户程序(如应用程序和服务)运行的状态。
操作系统=操作系统内核+操作系统配套的应用程序
假设现在要去银行办银行卡,那么银行内部人员就是内核态,前往办卡的就为用户态。
如果我们想要办卡,就走到小窗口向柜员说明情况,此时柜员就会帮你进行操作。但如果此过柜员问你要身份证复印件,如果没有,那么就需要去进行打印。
若请求柜员帮你进行打印,那么柜员什么时候打印,这个是不确定的,效率较低。这种就是用户态+内核态,调用系统API,由系统内核来完成这一系列操作。
若我们自己去自助复印机进行复印,整个过程就是连贯可控的,效率较高。这种就是纯用户态,由我们自己控制。
线程池的优点
- 减少资源消耗:通过重复利用已创建的线程,减少了创建和销毁线程所需的资源开销。
- 提高响应速度:当有新的任务到达时,若线程池中有空闲的线程,任务可以直接执行,无需等待新的线程创建。
- 防止资源耗尽:通过限制线程池中的最大线程数,可以避免因创建过多线程而引起的资源耗尽问题。
- 提高线程利用率:线程池中的线程可以被重复利用。
线程池最大的好处就是减少每次启动、销毁线程的损耗。
ThreadPollExecutor中的构造方法
在java中,给我们提供了现成的线程池供我们来使用。
我们可以看一下标准库中的线程池,在java.util.concurrent中给我们提供线程池的相关方法。
在点开concurrent之后,我们在接口中往下划,找到ThreadPoolExecutor类。我们可以看到,ThreadPoolExecutor类的构造方法有着好几个参数,那么这些参数都是什么意思呢?
‘’
我们拿第四个构造方法来看。
corePoolSize && maximumPoolSize
corePoolSize:即核心线程数
maximumPoolSize:最大核心线程数
线程池可以支持“线程扩容”,若某个线程池初始状态下有m个线程,但m若不够用,就会自动增加m的个数。
在java标准库的线程池中,把线程分为两类:核心线程和非核心线程。
核心线程可以理解为最少有多少个线程,而非核心线程就是线程扩容新增的线程。
核心线程数会始终存在线程池内部,而非核心线程,在繁忙的时候就会被创建出来,在空闲时就会把这些线程释放掉。
最大核心线程数=核心线程数+非核心线程数的最大值
keepAliveTime && unit
那么非核心线程在空闲的时候就会被立即释放掉吗?
若在空闲一会后,又有新的任务,那岂不是要重新新增线程。
所以在构造方法中,给我们提供了两个参数,用来设置非核心线程在空闲时等待任务的最长时间。
keepAliveTime:表示线程池中空闲的非核心线程在终止前等待新任务的最长时间。
unit:用来设置keepAliveTime参数的时间单位。
workQueue
我们可以看到workQueue的类型为BlockingQueue<Runnable>,说明这是个用来存放任务的阻塞队列。可以根据需要设置阻塞队列的类型,如果需要优先级,则可以使用PriorityBlockingQueue;如果不需要优先级且任务的数目是恒定的,则可以使用ArrayBlockingQueue;如果任务的数目不是恒定的,则可以使用LinkedBlockingQueue。
作用:
- 任务缓存:当线程池中的线程都在执行任务时,新提交的任务会被暂时存储在工作队列中,直到有空闲线程来执行它们。
- 任务调度:工作队列可以帮助线程池按照一定的策略来调度任务的执行顺序。
- 资源管理:通过限制工作队列的大小,可以控制线程池中等待执行的任务数量,从而避免资源耗尽的情况。
threadFactory
threadFactory:创建线程的⼯⼚,参与具体的创建线程⼯作.通过不同线程⼯⼚创建出的线程相当于 对⼀些属性进⾏了不同的初始化设置。
*handler(类型RejectedExecutionHandler)
最后一个参数是有关队列满后不要进行阻塞,而是要进行拒绝了,我们可以在构造方法上面查看线程池的拒绝策略有哪些.
在java提供了4种拒绝策略:
- ThreadPoolExecutor.AbortPolicy:有新的任务想要入队时,直接抛出异常
- ThreadPoolExecutor.CallerRunsPolicy:新添加的任务线程池拒绝执行,由添加任务的线程执行该任务
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃掉最旧的未被处理的请求,执行新的任务
- ThreadPoolExecutor.DiscardPolicy:丢弃掉当前新加的任务,不执行
讲完了线程池中的构造方法以及拒绝策略,那么如何在java中来使用线程池呢?
如何在java中使用线程池
1.创建线程池对象
ExecutorService ex=Executors.newFixedThreadPool(4);
ExecutorService是一个继承于Executor的接口,主要用来管理和控制线程,是Java并发编程的重要工具。
在java标准库中,虽然ThreadPoolExecutor功能强大,但是使用起来比较麻烦,所以java标准库对这个类进行了封装:Executors类
Executors类:是Java中用于创建线程池的工厂类,它提供了一系列的静态工厂方法,用于创建不同类型的线程池。
我们可以看到Executors工厂类中的工厂方法有以下几种类型:
- newCachedThreadPool():创建一个可缓存的线程池。这个线程池的线程数量可以根据需要自动扩展,如果有可用的空闲线程,就会重用它们;如果没有可用的线程,就会创建一个新线程。适用于执行大量的短期异步任务。
- newFixedThreadPool(int nThreads):创建一个固定大小的线程池,其中包含指定数量的线程。线程数量是固定的,不会自动扩展。适用于执行固定数量的长期任务。
- newSingleThreadExecutor():创建一个单线程的线程池。这个线程池中只包含一个线程,用于串行执行任务。适用于需要按顺序执行任务的场景。
- newScheduledThreadPool(int corePoolSize):创建一个固定大小的线程池,用于定时执行任务。线程数量固定,不会自动扩展此线程池;可以安排在给定延迟后运行命令或者定期执行。
- newSingleThreadScheduledExecutor():创建一个单线程的定时执行线程池。只包含一个线程,用于串行定时执行任务。
- newWorkStealingPool(int parallelism):创建一个工作窃取线程池,线程数量根据CPU核心数动态调整。这种线程池适合处理大量细粒度的可取消任务。
2.调用submit添加任务
这里我们创建的是一个固定线程数为4的线程池,在线程池中,我们想要添加任务需要调用submit()方法,可以看到需要的参数是一个Runnable类型的任务。
我们可以使用Lambda表达式或者直接new Runnable。(这里用第一个)
for(int i=0;i<100;i++) {int id = i;ex.submit(() -> {System.out.println(Thread.currentThread().getName() + " 任务:" + id);});}
当我们运行之后,会发现任务都结束后,程序还没有停止,这是为什么呢?
在 Java 中,线程池创建的线程默认为前台线程,虽然main线程结束了,但是线程池里的前台线程仍然存在。
3.调用shutdown关闭线程池
如果我们想要关闭线程池中的线程,我们可以使用shutdown方法。
public static void main(String[] args) {ExecutorService ex=Executors.newFixedThreadPool(4);for(int i=0;i<100;i++) {int id = i;ex.submit(() -> {System.out.println(Thread.currentThread().getName() + " 任务:" + id);});}ex.shutdown();}
既然我们学会如何使用java中的线程池,那么我们可以来实现一个线程池.
手动实现线程池
创建一个线程池类
由于这里是使用的阻塞队列,在队列满的时候若添加新的任务,会进入阻塞等待状态,不同于java线程池里的拒绝策略。
/*** 自定义线程池执行器类* 该类通过实现一个具有固定大小的线程池和一个阻塞队列来管理线程,用于异步执行任务*/
class MyThreadPoolExecutor {// 创建阻塞队列,用于存放待执行的任务// 队列大小设为1000,用于控制并发任务的数量,避免过多任务导致资源耗尽BlockingQueue<Runnable> blockingQueue=new ArrayBlockingQueue<>(1000);/*** 构造函数,初始化线程池* 创建一个线程,该线程循环从阻塞队列中取任务并执行* 这个线程是线程池中的工作线程,负责执行提交的任务*/public MyThreadPoolExecutor(int n) {for (int i = 1; i <= n; i++) {Thread t = new Thread(() -> {// 无限循环,确保线程池可以持续处理任务,直到程序中断或阻塞队列被清空while (true) {try {// 从阻塞队列中取出一个任务,如果队列为空,则线程被阻塞,直到有任务放入队列Runnable task = blockingQueue.take();// 执行取出的任务task.run();} catch (InterruptedException e) {// 如果线程在等待状态时被中断,抛出运行时异常// 这通常会导致程序异常终止throw new RuntimeException(e);}}});// 启动线程池中的工作线程t.start();}}/*** 提交一个任务到线程池* @param task 需要被执行的任务* 任务被放入阻塞队列中,随后由线程池中的工作线程执行*/public void submit(Runnable task){// 将任务放入阻塞队列,如果队列已满,则操作会阻塞,直到有空间可用blockingQueue.offer(task);}
}
测试
class DemoTest1{public static void main(String[] args) throws InterruptedException {MyThreadPoolExecutor ex=new MyThreadPoolExecutor(4);for(int i=0;i<100;i++) {int id = i;ex.submit(()->{System.out.println(Thread.currentThread().getName()+" 任务:"+id);});}}
}
本篇就先到这了~
若有不足,欢迎指正~