一、使用线程池的好处

①每次创建和销毁线程需要占用时间和资源,通过线程池,可以复用已有的线程处理新的问题,避免了频繁创建和销毁线程。

②增强系统稳定性。线程池可以指定并发线程数(不限制并发线程数的话,同一时间运行很多线程的话,可能会导致系统内存不足),并且超过指定并发数之后,可以通过拒绝策略,保证系统正常运行。

二、核心线程数的大小如何设置

一般根据CPU的 核心数N 指定核心线程数的大小。如果任务涉及运算,一般指定 N + 1 个核心线程数。如果任务涉及数据库或者第三方调用,一般指定 2 x N 个核心线程数。

线程会占用一定的内存,核心线程设置过大,可能导致内存溢出,实际指定多少合适,可以进行压力测试来验证。

三、 ThreadPoolExecutor 的工作原理

JAVA的标准线程池ThreadPoolExecutor核心设计逻辑:同一个线程池共享阻塞队列,即多个线程从同一个阻塞队列中拉取任务去执行。

首先考虑 核心线程处理:优先使用核心线程(corePoolSize)执行任务。

其次考虑 队列缓冲:如果核心线程已满,任务进入阻塞队列等待。

最后才考虑 创建非核心线程:仅当阻塞队列已满,且当前线程数未达最大线程数(maximumPoolSize)时,才会创建新线程。当到达最大线程数,就会触发拒绝策略,默认的拒绝策略是RejectedExecutionException,可以自定义拒绝策略。

四、模拟超过最大线程数

最大线程数我故意设置成7,阻塞队列使用 有界的LinkedBlockingQueue,指定容量为4,并且自定义了拒绝策略。

import java.util.concurrent.*;

public class ThreadPoolExecutorDemo1 {

//核心线程数,核心线程默认会一直存在

public static int corePoolSize = 5;

//最大线程数

public static int maximumPoolSize = 7;

//非核心线程的线程存活时间

public static int keepAliveTime = 2;

public static void main(String[] args) {

ThreadPoolExecutor executor = new ThreadPoolExecutor(

corePoolSize,

maximumPoolSize,

keepAliveTime, TimeUnit.SECONDS,

new LinkedBlockingDeque<>(4), //有界队列,容量4

Executors.defaultThreadFactory(),

new RejectedExecutionHandler() {

@Override

public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

System.out.println("线程超过最大线程数");

}

});

//模拟并发执行30次任务

for (int i = 0; i < 30; i++) {

executor.execute(() -> {

//执行异步业务流程

System.out.println(Thread.currentThread().getName() +

" 执行异步业务流程,阻塞队列剩余容量:" + executor.getQueue().remainingCapacity() +

" 阻塞队列已存储的任务数量:" + executor.getQueue().size());

//阻塞队列的内存地址是一样的,说明所有工作线程使用的是同一个列

// System.out.println(System.identityHashCode("阻塞队列的内存地址:" + executor.getQueue()));

});

}

System.out.println("==================主线程执行完毕===================");

}

}

控制台打印如下:

五、ArrayBlockingQueue 和 LinkedBlockingQueue 的区别

这两个都是阻塞队列,但常用的是LinkedBlockingQueue

线程池ThreadPoolExecutor的execute和submit方法,实际上是提交一个任务,可以理解为生产者。而消费者,也就是真正去执行提交的这个任务,是交给线程池中的工作线程(Worker Thread)去做,它会通过getTask方法,从阻塞队列获取任务并去执行。

ArrayBlockingQueue数据结构是数组,使用单一锁,也就是 生产者 和 消费者使用同一个锁,这导致生产和消费没办法并行执行,消费者获取任务时会受到单一锁的影响,吞吐量小。

LinkedBlockingQueue数据结构是链表,使用分离锁。生产者用一把锁,消费者用一把锁,高并发下,吞吐量大。

只要线程池中的核心线程数量corePoolSize没有超过指定大小,就不会将任务放入阻塞队列,这个时候,不会受到锁的影响。而一旦阻塞队列有数据,核心线程从阻塞队列获取任务的时候,才会受到锁的影响。

六、LinkedBlockingQueue的注意事项

使用LinkedBlockingQueue时,不要使用默认的无参构造,那样创建的是无界队列。最好指定阻塞队列的容量,创建有界队列。

而无界队列会存在以下问题:

①队列永远不会被填满,不会触发线程数扩展,永远使用核心线程进行处理。

②不会触发拒绝策略,可能会导致内存溢出