线程池使用及优势
线程池的主要工作是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数超过了最大数量,超出数量的线程就需要排队等候,等待其他线程执行完毕
它的主要特点可以总结为:线程复用,控制最大并发数,管理线程
线程池主要优势又如下三点:
- 可以降低资源消耗,通过重复使用已经创建的线程避免多次创建和销毁线程所带来的性能开销
- 可以提高响应速度,任务到达时,如果有空闲线程可以直接执行,而不需要等待线程创建时间
- 提高线程的可管理性,线程是稀缺资源,如果对于线程的创建和销毁不加以管理,不仅会消耗系统资源,并且会降低系统的稳定性,使用线程池可以对线程进行统一的分配、调节和监控
线程池的常用方式
Java中的线程池使通过Executor
框架实现的,使用线程池用到了Executor
,Executors
,ExecutorService
,ThreadPoolExecutor
这几个类
其中Executors
是一个工厂方法,提供了快捷创建线程池的方法,常用的线程池又如下几种:
-
Executors.newFixedThreadPool(int nThread)
:固定线程数的线程池,参数nThread
为线程池的线程数,如果线程全部忙碌,新提交的任务会在队列中等待创建一个大小为4的线程池,并提交10个任务执行
public class ThreadPoolDemo { public static void main(String[] args) { fixedThreadPool(); } private static void fixedThreadPool() { ExecutorService threadPool = Executors.newFixedThreadPool(4); try { for (int i = 0; i < 10; i++) { threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + " run"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); } } }
结果:
pool-1-thread-1 run
pool-1-thread-2 run
pool-1-thread-3 run
pool-1-thread-4 run
pool-1-thread-1 run
pool-1-thread-2 run
pool-1-thread-3 run
pool-1-thread-4 run
pool-1-thread-1 run
pool-1-thread-2 run可以看到最多只有4个线程在执行任务
-
Executors.newSingleThreadExecutor()
:单个线程的线程池public class ThreadPoolDemo { public static void main(String[] args) { singleThreadPool(); } private static void singleThreadPool() { ExecutorService threadPool = Executors.newSingleThreadExecutor(); try { for (int i = 0; i < 10; i++) { threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + " run"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); } } }
结果:
pool-1-thread-1 run
pool-1-thread-1 run
pool-1-thread-1 run
pool-1-thread-1 run
pool-1-thread-1 run
pool-1-thread-1 run
pool-1-thread-1 run
pool-1-thread-1 run
pool-1-thread-1 run
pool-1-thread-1 run只有一个线程执行任务
-
Executors.newCachedThreadPool()
:无限大小的线程池public class ThreadPoolDemo { public static void main(String[] args) { cachedThreadPool(); } private static void cachedThreadPool() { ExecutorService threadPool = Executors.newCachedThreadPool(); try { for (int i = 0; i < 10; i++) { threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + " run"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); } } }
结果:
pool-1-thread-1 run
pool-1-thread-3 run
pool-1-thread-2 run
pool-1-thread-4 run
pool-1-thread-5 run
pool-1-thread-7 run
pool-1-thread-8 run
pool-1-thread-6 run
pool-1-thread-9 run
pool-1-thread-10 run同时有多少任务在执行,就有多少线程
通过查看这三个工厂方法的源码得知:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
底层都是创建了ThreadPoolExecutor
对象,该类的构造方法有7个参数:
-
int corePoolSize
: 线程池中常驻的核心线程数,当线程池线程数达到该值时,就会将任务放入队列 -
int maximumPoolSize
: 线程池中能容纳的同时执行的最大线程数,必须大于等于1 -
long keepAliveTime
: 多余空闲线程的存活时间,当前线程数大于corePoolSize
且空闲时间达到该时间值时,多余线程会被销毁 -
TimeUnit unit
:keepAliveTime
的时间单位 -
BlockingQueue<Runnable> workQueue
: 任务队列,保存提交但尚未执行的任务 -
ThreadFactory threadFactory
: 线程池中创建 工作线程的工厂,一般使用默认工厂 -
RejectedExecutionHandler handler
: 拒绝策略,当队列满时且工作线程等于最大线程数并的处理策略
线程池的工作流程
线程池的工作流程如下:
- 在创建了线程池后,等待任务提交
- 当调用
execute()
提交任务时,线程池做出如下判断:- 如果正在运行的线程数小于
corePoolSize
,立刻创建线程执行任务 - 如果正在运行的线程数等于
corePoolSize
,将任务放入队列 - 如果队列已满并且运行的线程数小于
maximumPoolSize
,创建非核心线程执行任务 - 如果队列已满并且运行的线程数等于
maximumPoolSize
,按照饱和拒绝策略拒绝新任务
- 如果正在运行的线程数小于
- 当一个线程执行完成后,会从队列中取下一个任务来执行
- 当一个线程没有运行超过
keepAliveTime
时,线程池会判断:- 如果当前线程数大于
corePoolSize
,那么这个线程将会被销毁
- 如果当前线程数大于
拒绝策略
当线程池中队列已满且工作线程达到最大数量时,线程池会拒绝新任务的提交直至队列出现空位或有空闲线程,对于拒绝的任务有不同的处理方式,称为拒绝策略。
线程池提供了四种拒绝策略:
-
AbortPolicy
:直接抛出RejectedExecutionException
异常,该策略为默认策略 -
CallerRunsPolicy
:”调用者运行策略“,该策略即不会抛弃任务,也不会抛出异常,而是将某些任务回退至调用者,从而降低新的任务流量 -
DiscardOldestPolicy
:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次尝试提交 -
DiscardPolicy
:直接丢弃任务,不予处理也不抛出异常。如果允许任务丢失,这是最好的一种方案
以上拒绝策略均实现了RejectedExecutionHandler
接口
如何配置线程池
-
CPU密集型
CPU密集型任务需要大量的运算,CPU长期保持高负载,阻塞时间较少
那么对于CPU密集型任务,需要通常配置较少的线程数量,一般核心线程数设置为CPU核心数,减少线程上下文的切换
-
IO密集型
IO密集型任务需要大量的IO,也就意味着大量的阻塞,所以在单个线程上运行IO密集型任务会因为等待IO结束导致浪费大量的CPU运算能力
所以在IO密集型任务中使用多线程可以大大加速程序运行,可以配置较多的线程
参考公式为:核心线程数=CPU核心数/(1-阻塞系数)
阻塞系数:0.8~0.9
例如8核心的CPU,则设置核心线程数为8/(1-0.9)=80