Java线程池
Executor 类图
线程池原理
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果阻塞队列满了,那就创建新的线程执行当前任务;直到线程池中的线程数达到 maxPoolSize,这时再有任务来,只能执行 reject() 处理该任务。
线程池种类
newFixedThreadPool()
初始化一个指定线程数的线程池,其中 corePoolSize == maxiPoolSize,使用 LinkedBlockingQuene 作为阻塞队列 特点:即使当线程池没有可执行任务时,也不会释放线程。
newCachedThreadPool()
初始化一个可以缓存线程的线程池,默认缓存 60s,线程池的线程数可达到 Integer.MAX_VALUE,即 2147483647,内部使用 SynchronousQueue 作为阻塞队列; 特点:在没有任务执行时,当线程的空闲时间超过 keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销; 因此,使用时要注意控制并发的任务数,防止因创建大量的线程导致而降低性能。
newSingleThreadExecutor()
初始化只有一个线程的线程池,内部使用 LinkedBlockingQueue 作为阻塞队列。 特点:如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行。
newScheduledThreadPool()
特点:初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。
ThreadPoolExecutor()
默认线程池,可控制参数比较多,实际上是前面四种的模板,自定义时只需传入自己想要的参数即可。
初始化示例
1 | // 使用Executors静态方法进行初始化 |
常用方法
- execute 与 submit 的区别
- execute 只能提交 Runnable 类型的任务,而 submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务。
- execute 会直接抛出任务执行时的异常,submit 会吃掉异常,可通过 Future 的 get 方法将任务执行时的异常重新抛出。
- submit 有返回值,而 execute 没有。
- shutDown 与 shutDownNow 的区别
- shutDown:当线程池调用该方法时,线程池的状态则立刻变成 SHUTDOWN 状态。此时,则不能再往线程池中添加任何任务,否则将会抛出 RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
- shutDownNow:相当于调用每个线程的 interrupt() 方法。
- 其他方法
- prestartAllCoreThreads():提前创建并启动所有核心线程。
- setCorePoolSize() 和 setMaximumPoolSize():动态调整线程池容量大小。
关键参数
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:线程存活时间(在 corePore < * < maxPoolSize 情况下有用)
workQueue:阻塞队列(用来保存等待被执行的任务)
handler:当拒绝处理任务时的策略
四大阻塞队列(workQueue)
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,按 FIFO 排序任务
- LinkedBlockingQuene:基于链表结构的阻塞队列,按 FIFO 排序任务
- SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 ArrayBlockingQuene
- PriorityBlockingQuene:具有优先级的无界阻塞队列
四大拒绝策略(handler)
- ThreadPoolExecutor.AbortPolicy:默认,队列满了丢弃任务并抛出 RejectedExecutionException 异常
- ThreadPoolExecutor.DiscardPolicy:队列满了丢弃任务,但是不抛出异常
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试加入队列(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:如果添加到线程池失败,那么主线程会自己去执行该任务
合理设置线程池大小
线程等待时间所占比例越高,需要越多线程。线程 CPU 时间所占比例越高,需要越少线程。
一般需要根据任务的类型来配置线程池大小: 如果是 CPU 密集型任务,就需要尽量压榨 CPU,参考值可以设为 NCPU+1;如果是 IO 密集型任务,参考值可以设置为 2*NCPU。
补充:CPU 密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠 CPU 的运算能力;涉及到网络、磁盘 IO 的任务都是 IO 密集型任务,这类任务的特点是 CPU 消耗很少,任务的大部分时间都在等待 IO 操作完成(因为 IO 的速度远远低于 CPU 和内存的速度)。