目录
1 概述
Java中经常出现多线程以及高并发的场景,线程的频繁创建在高并发及大数据量是非常消耗资源的,因此java提供了线程池。
线程池的作用:线程池就是限制系统中使用线程的数量以及更好的使用线程。创建一定数量的线程置于线程池中,这些运行着线程不断去去获取任务并运行,当没有任务时,就等待。
线程池的优点:
- 减少线程创建和销毁的次数,使线程可以多次复用
- 可以根据系统情况,调整线程的数量。防止创建过多的线程,消耗过多的内存
要讲线程池,需要先讲一下Java里的Executor框架
2 Executor框架
常用的接口与类:
Executor框架是一种将线程的创建和执行分离的机制。基于Executor和ExecutorService接口,及这两个接口的实现ThreadPoolExecutor展开。Executor有一个内部线程池,通过方法将并提供了将Runnable接口实现的任务和通过Callable接口实现的任务传递到池中线程以获得执行。
2.1 ThreadPoolExecutor
ThreadPoolExecutor类是线程池中最核心的一个类,它的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
构造器的参数:
- corePoolSize:核心线程数,如果运行的线程少于corePoolSize,则创建新线程来执行新任务。当线程池中的线程数达到 corePoolSize后,就会把到达的任务放到缓存队列当中
- maximumPoolSize:线程池最大线程数,表示在线程池中最多能创建多少个线程
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。只有当线程池中的线程数大于corePoolSize时,keepAliveTime才起作用,直到线程池中的线程数不大于corePoolSize
- unit:参数keepAliveTime的时间单位,有7种值:TimeUnit.MILLISECONDS、TimeUnit.SECONDS等
- workQueue:一个阻塞队列,用来存储等待执行的任务,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:LinkedBlockingQueue,SynchronousQueue,ArrayBlockingQueue,PriorityBlockingQueue等
- threadFactory:线程工厂,主要用来创建线程
- handler:表示当拒绝处理任务时的策略
ThreadPoolExecutor中,除了构造参数外,还有几个重要的成员变量:
//线程池的主要状态所,修改线程池的一些成员变量都要用这个锁,比如线程池状态,或者下面这些字段等
private final ReentrantLock mainLock = new ReentrantLock();
//worker的集合,用来存放worker
private final HashSet<Worker> workers = new HashSet<Worker>();
//设置是否允许核心线程过期,默认false,即keepalivetime对核心线程不生效
private volatile boolean allowCoreThreadTimeOut;
//记录完成的任务数量
private long completedTaskCount;
//记录线程池中出现过的最大的线程数量
private int largestPoolSize;
2.1.1 线程池状态
ThreadPoolExecutor中,线程池有五种状态:
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
五种状态之间的切换如下:
- RUNNING:线程池被一旦被创建,就处于RUNNING状态,能够接收新任务,以及对已添加的任务进行处理。
- SHUTDOWN:通过 调用shutdown()方法, 线程池切换到SHUTDOWN状态,不接收新任务,它会等待所有任务执行完毕
- STOP:通过调用shutdownNow()方法,线程池切换到STOP状态,不接收新任务,并且会去尝试终止正在执行的任务
- TIDYING:当所有的任务已终止,线程池会变为TIDYING状态,会执行钩子函数terminated(),terminated()在ThreadPoolExecutor类中是空的,可以重写。
- TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,线程池彻底终止,就变成TERMINATED状态。
2.1.2 线程池增长策略
- 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
- 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,创建新的线程来处理被添加的任务。
- 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
即处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
2.1.3线程池的拒绝策略
由构造器参数RejectedExecutionHandler handler表示当拒绝处理任务时的策略,有以下四种取值:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionHandler
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前端的任务,然后重新尝试执行任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
2.1.4 线程池的线程初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
- prestartCoreThread():初始化一个核心线程;
- prestartAllCoreThreads():初始化所有核心线程
2.1.5 任务缓存队列
workQueue的类型为BlockingQueue<Runnable>,通常可以取下面三种类型:
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小,支持公平锁和非公平锁;
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
- synchronousQueue:一个不存储元素的阻塞队列,将直接新建一个线程来执行新来的任务,每一个put操作必须等待take操作,否则不能添加元素
- PriorityBlockingQueue:支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则
2.1.6 线程池的原理
线程池其实是一个生产者-消费者模型,如下图:
通过调用Executor的execute()方法(或者submit(),submit()也是调用了execute()方法),线程池将任务提交到阻塞队列里。
而另一边,worker线程不断地从队列中取出任务,执行任务的run()方法运行线程。
2.1.7 几种线程池实现
用户可以不自己创建ThreadPoolExecutor
其中,Executors类以静态工厂方法提供了四种线程池的实现:
- CachedThreadPool:可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- FixedThreadPool :固定线程数量的线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- ScheduledThreadPool :固定线程数量的线程池,支持定时及周期性任务执行。
- SingleThreadExecutor :单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
2.1.8 线程池大小配置
一般需要根据任务的类型来配置线程池大小,假设N为CPU数量:
- 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 N+1
- 如果是IO密集型任务,参考值可以设置为2*N
当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。