线程池可以重用线程资源,减少线程创建销毁的资源耗费。JDK 提供了 ThreadPoolExecutor 来创建线程池。
public ThreadPoolExecutor(int corePoolSize, // 线程池初始大小
int maximumPoolSize, // 线程池最大线程数
long keepAliveTime, // 线程存活的时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务等待队列
ThreadFactory threadFactory, // 线程工厂用来创建线程
RejectedExecutionHandler handler // 线程数量超过线程池大小后的处理策略
) {}
线程池参数效果
我们要创建一个核心线程数 5 ,最大线程数 20 ,队列大小是 10 的一个线程池。
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 20,
5, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5), Executors.defaultThreadFactory(), defaultHandler);
如果我们提交 10 个任务,那么首先会将前 5 个任务交给 5 个线程执行,同时 5 个线程进入队列等待。这时队列有 5 个线程工作,5 个任务等待。
如果我们提交 15 个任务,那么首先会将前 5 个任务交给 5 个线程执行,同时 5 个线程进入队列等待,再新创建 5 个线程来执行 11-15 号任务。这时队列有 10 个线程工作,5 个任务等待。
如果我们提交 50 个任务,那么首先会将前 5 个任务交给 5 个线程执行,同时 5 个线程进入队列等待,再新创建 15 个线程来执行任务,剩下的 25 个任务会丢弃掉。这时队列有 20 个线程工作,5 个任务等待,等任务执行完,超过线程存活时间,线程会被销毁,剩下 5 个线程。
jdk 提供的线程池
除了使用 new ThreadPoolExecutor ,Executors 提供了一些静态方法用来创建线程池,如:
Executor executor = Executors.newSingleThreadExecutor();
//new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
Executor executor = Executors.newFixedThreadPool(5);
//new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
Executor executor = Executors.newCachedThreadPool();
//new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
Executor executor = Executors.newScheduledThreadPool();
本质上和我们自己 new 一个线程池是一样的。
LinkedBlockingQueue 是阻塞队列,会缓存任务。SynchronousQueue 不缓存任务,会将任务直接发给消费端执行。所以如果我们创建以下线程池:
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 20L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadPoolExecutor.AbortPolicy());
当我们提交 50 个任务,那么只有 20 个任务能够执行,剩下 30 个线程会被丢弃。
虽然 JDK 提供了 Executors 几种创建线程的方式,但是实际使用中并不建议直接使用,因为 LinkedBlockingQueue 虽然是有界队列,但是队列大小默认是 Integer.MAX_VALUE ,如果任务很多会导致 OOM 。所以我们还是需要根据实际情况自己创建线程池。
线程的数量需要根据实际情况设置,如果任务是 CPU 密集型,那么线程数和 CPU 核数相同,可以减少线程上下文切换,如果是 IO 密集型,线程数量可以是 CPU 核数的若干倍。这个数据只是经验值,实际多少最好需要压测。