Java线程池

1、注意异常的捕获

线程池在使用时,在使用ThreadPoolExecutor时,例如
ThreadPoolExecutor.execute(new Runnable());
如果不捕捉异常,那么容易造成异常的丢失,例如线程池如果队列已满,并且达到了最大线程数,那么线程会拒绝执行任务,这个时候如果不去捕捉execute()的异常,则容易造成任务未完成,也没有任何异常,所以执行时可以选择捕获到异常。

try {
    ThreadPoolExecutor.execute(new Runnable());
}catch (Exception e){
    log.warn("executor exception:",e);
    //补偿执行
    doCompensationWork();
}

2、线程池创建方式的选择:

当需要创建线程池时,Java可以选择两种创建方式

2.1 通过Executors创建线程池:

但是通过Executors创建线程池是存在风险,原因在于:(摘自阿里编码规约)
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

2.2 使用new ThreadPoolExecutor创建线程池创建方法:

其实Executors底层也是使用ThreadPoolExecutor进行创建的,使用ThreadPoolExecutor创建需要自己指定核心线程数、最大线程数、线程的空闲时长以及阻塞队列。这样可以按照实际使用场景设定需要的核心线程以及队列大小,避免出现内存占用过多或者线程过多造成的OOM风险,所以优先使用ThreadPoolExecutor创建线程池。

3 Executors各个方法的弊端的原因

3.1 newFixedThreadPool和newSingleThreadExecutor:

主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
查看其源码可以看到newFixedThreadPool、newSingleThreadExecutor都是使用LinkedBlockingQueue作为线程池的队列,这样核心线程都占满后,任务会存储到队列,这样就可能导致队列存储了多个任务,造成内存泄漏甚至OOM的风险。

Java线程池_第1张图片

Java线程池_第2张图片

3.2 newCachedThreadPool和newScheduledThreadPool:

主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

Java线程池_第3张图片

Java线程池_第4张图片

4、ThreadPoolExecutor参数解析:

1、corePoolSize & maximumPoolSize核心线程数(corePoolSize)和最大线程数(maximumPoolSize)

当一个新任务被提交到池中,如果当前运行线程小于核心线程数(corePoolSize),即使当前有空闲线程,也会新建一个线程来处理新提交的任务;如果当前运行线程数大于核心线程数(corePoolSize)并小于最大线程数(maximumPoolSize),只有当等待队列已满的情况下才会新建线程,即大于核心线程时,优先放在队列,队列已满才会去创建新的线程,直至到达最大线程,这时再来任务,则执行设定的拒绝策略;

2、keepAliveTime & unitkeepAliveTime

为超过 corePoolSize 线程数量的线程最大空闲时间,unit 为时间单位。非核心线程的最大空闲时间;

3、等待队列任何阻塞队列(BlockingQueue)

都可以用来转移或保存提交的任务,线程池大小和阻塞队列相互约束线程池:如果运行线程数小于corePoolSize,提交新任务时就会新建一个线程来运行;如果运行线程数大于或等corePoolSize,新提交的任务就会入列等待;如果队列已满,并且运行线程数小于maximumPoolSize,也将会新建一个线程来运行;如果线程数大于maximumPoolSize,新提交的任务将会根据拒绝策略来处理。

4、三种通用的入队策略

直接传递:通过 SynchronousQueue 直接把任务传递给线程。如果当前没可用线程,尝试入队操作会失败,然后再创建一个新的线程。当处理可能具有内部依赖性的请求时,该策略会避免请求被锁定。直接传递通常需要无界的最大线程数(maximumPoolSize),避免拒绝新提交的任务。当任务持续到达的平均速度超过可处理的速度时,可能导致线程的无限增长。

无界队列:使用无界队列(如 LinkedBlockingQueue)作为等待队列,当所有的核心线程都在处理任务时, 新提交的任务都会进入队列等待。因此,不会有大于 corePoolSize 的线程会被创建(maximumPoolSize 也将失去作用)。这种策略适合每个任务都完全独立于其他任务的情况;例如网站服务器。这种类型的等待队列可以使瞬间爆发的高频请求变得平滑。当任务持续到达的平均速度超过可处理速度时,可能导致等待队列无限增长。

有界队列:当使用有限的最大线程数时,有界队列(如 ArrayBlockingQueue)可以防止资源耗尽,但是难以调整和控制。队列大小和线程池大小可以相互作用:使用大的队列和小的线程数可以减少CPU使用率、系统资源和上下文切换的开销,但是会导致吞吐量变低,如果任务频繁地阻塞(例如被I/O限制),系统就能为更多的线程调度执行时间。使用小的队列通常需要更多的线程数,这样可以最大化CPU使用率,但可能会需要更大的调度开销,从而降低吞吐量。

5、拒绝策略

当线程池已经关闭或达到饱和(最大线程和队列都已满)状态时,新提交的任务将会被拒绝。ThreadPoolExecutor 定义了四种拒绝策略:

AbortPolicy:默认策略,在需要拒绝任务时抛出RejectedExecutionException;

CallerRunsPolicy:直接在 execute 方法的调用线程中运行被拒绝的任务,如果线程池已经关闭,任务将被丢弃;

DiscardPolicy:直接丢弃任务;DiscardOldestPolicy:丢弃队列中等待时间最长的任务,并执行当前提交的任务,如果线程池已经关闭,任务将被丢弃。

我们也可以自定义拒绝策略,只需要实现 RejectedExecutionHandler;需要注意的是,拒绝策略的运行需要指定线程池和队列的容量。

你可能感兴趣的:(java)