Executor与线程池:如何创建正确的线程池?

it2022-05-05  75

在各大公司的面试中,线程池的题目都是比较多且比较难的,并且,线程相关的对象和其他的业务API是不相同的,区别在于一个直接操纵了操作系统,使用的是操作系统相关的API,一个单纯只占用内存。

从Java核心专栏线程相关的知识中我们也可以知道,线程的产生与销毁都会消耗一定的性能,所以要避免频繁的创建与销毁。

那么解决相关问题的方法就是,创建线程池。

概述

线程池的需求很普遍,从一般使用的池化角度去说:当你需要资源的时候就用acquire()方法来申请资源,用完之后就调用release()释放资源。但是在线程池中,是完全不同的,Java没有提供申请线程和释放线程的方法。

线程池模型:生产-消费

为什么线程池的模型和普通的池化资源不同呢,如果采用了一般模型的线程池设计,那应该是是如下:

class ThreadPool{ //获取空闲线程 Thread acquire() { } //释放空闲线程 void release(Thread t) { } } ThreadPool pool; Thread t1 = pool.acquire(); //传入Runnable对象 t1.execute(()->{ //业务代码 });

过程:假设我们获取到一个空闲线程T1,然后使用t1完成我们的业务。

创建线程t1调用t1.execute()然后传入Runnable执行基本逻辑,就像通过构造函数Thread(Runnable target)创建线程。

但是并没有execute(Runnable target)这个方法。所以线程池并不是这么设计的。

线程池最终采用的设计方式为 生产-消费

线程池的使用方式生产者,线程池本身是消费者。 可以看以下的示例代码:

package jike_Time; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; //简化的线程池,仅仅用来说明工作原理 public class MyThreadPool { //利用阻塞队列来实现生产者-消费者模型 BlockingQueue<Runnable> workQueue; //保存内部工作线程 List<WorkerThread> threads = new ArrayList<>(); //构造方法 MyThreadPool(int poolSize, BlockingQueue<Runnable> workQueue) { this.workQueue = workQueue; //创建工作线程 for (int idx = 0; idx < poolSize; idx++) { WorkerThread workerThread = new WorkerThread(); workerThread.start(); threads.add(workerThread); } } //工作线程负责消费任务,并且执行任务 class WorkerThread extends Thread { @Override public void run() { //循环取任务并且执行 while (true) { Runnable task = null; try { task = workQueue.take(); } catch (InterruptedException e) { e.printStackTrace(); } task.run(); } } } //提交任务 void execute(Runnable command) throws InterruptedException { workQueue.put(command); } } /*使用实例*/ //创建有界阻塞队列 class User { public static void main(String[] args) throws InterruptedException { BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2); //创建线程池 MyThreadPool pool = new MyThreadPool(10, workQueue); //提交任务 pool.execute(() ->{ System.out.println("hello"); }); } } //output : hello

接下来看一下程序,在MyThreadPool内部,我们维护了一个阻塞队列workQueue和一组工作线程,工作线程的个数由构造函数的poolSize决定。用户通过execute()方法提交Runnable任务,execute()方法内部仅仅是添加任务到任务队列。而MyThreadPool会消费任务队列执行任务,相关的代码就是while循环。

多线程编程无法throws异常

多说一嘴,多线程编程之所以无法throws抛出异常,用最简单的话来说,多线程中一个线程中止,不会影响 其他线程继续运行,所以Exception就会逃逸,在Java线程被设计出来的时候,有这样一个理念,线程自己的异常由线程自己解决,而线程的实现是在run方法内的,如果想捕获到单线程的异常,就需要在run方法内部catch,而不是在大类里面throws。 但,也并不是完全没有方法在不catch的情况下获得到线程内部的异常,java5有一个方法Thread.UncaughtExceptionHandler.uncaughtException(),可以帮助我们获取到线程内部的异常,但是很麻烦,简化之后,我们就参照正常的线程设计,去设计我们的架构就好。

如何使用java的线程池

Java的线程池中,最核心的就是ThreadPoolExecutor,此工具的构造函数十分复杂,如以下代码所示,共7个参数:

ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler )

然后来一一介绍一下里面的参数:

corePoolSize :表示线程池保有的最小线程数。一般给不太重要的业务使用。maximumPoolSize :表示线程池创建的最大线程数。给最繁忙的业务用,但是当业务清净下来,也会降低线程使用,但不会低于corePoolSize。keepAliveTime & unit :用来定义业务的空闲与繁忙,当超过keepAliveTime & unit(ms)都没有响应,就会定义为空闲,降低线程的占用。workQueue :工作队列,和上面示例代码的工作队列同义。threadFactory :通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字handler::通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队 列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略, 你可以通过handler这个参数来指定。以下是四种拒绝方法: CallerRunsPolicy:提交任务的线程自己去执行该任务。AbortPolicy:默认的拒绝策略,会throws RejectedExecutionException。DiscardPolicy:直接丢弃任务,没有任何异常抛出。DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入 到工作队列。

使用线程池要注意什么

因为上面提到的这些参数确实使用起来比较复杂,于是并发包就提供了一个静态并发工厂类Executors快速创建线程池,不过不建议。 原因是:Executors很多方法都是无界的LinkedBlockingQueue,无界队列想当然的会造成oom(Out of Memory),而oom会耽误所有事情,所以建议用有界队列。

当采用了有界队列,当并发量超载的时候,就会触发RejectedExecutionException拒绝策略,此异常不会被强制catch,容易中断 项目,所以要慎用,包括锁的降级同理,因此最好自定义降级策略和拒绝策略。

当使用线程池进行异常处理的时候,建议根据不同的异常,写出不同的策略。


最新回复(0)