摘要:
在之前的时候Java的线程既是工作单元,也是执行机制。但是从JDK1.5开始,就把工作单元和执行机制分开,工作单元包括Runnable和Callable,而执行机制就由Executor框架提供。
Executor框架的成员:
ThreadPoolExecutor
、ScheduledThreadPoolExecutor
、Future
接口、Runnable
接口、Callable
接口、Executors
。
这里在阿里编码规范里面提到过线程池使用的注意事项:
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
所以,我们需要显示的创建ThreadPoolExecutor,里面参数多,无所谓,搞懂就行,更好根据情况来设定自己需要的参数。
首先看一下构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//这里忽略那些赋值操作
}
- corePoolSize:线程池中的核心线程数(在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来 才创建线程去执行任务)
- maximumPoolSize:线程池中的最大线程数(最多只能创建这么多线程,如果还有任务过来一般直接放到阻塞队列里面去)
- keepAliveTime:线程池中的线程存活时间(准确来说应该是没有任务执行时的回收时间,后面会分析)
- unit:keepAliveTime的时间单位(在TimeUnit类中有7种静态属性)
- workQueue:来不及执行的任务存放的阻塞队列(线程池的排队策略与BlockingQueue有关。有3种不同的策略,)
- threadFactory:新建woker线程(注意不是我们提交的任务)是进行一些属性设置,比如线程名,优先级等等,有默认实现。
- handler: 任务拒绝策略,当运行线程数已达到maximumPoolSize,队列也已经装满时会调用该参数拒绝任务,有默认实现。
运行策略:
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行 的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
这个过程说明,并不是先加入任务就一定会先执行。假设队列大小为 4,corePoolSize为2,maximumPoolSize为6,那么当加入15个任务时,执行的顺序类似这样:首先执行任务 1、2,然后任务3~6被放入队列。这时候队列满了,任务7、8、9、10 会被马上执行,而任务 11~15 则会抛出异常。最终顺序是:1、2、7、8、9、10、3、4、5、6。当然这个过程是针对指定大小的ArrayBlockingQueue<Runnable>来说,如果是LinkedBlockingQueue<Runnable>,因为该队列无大小限制,所以不存在上述问题。
去扒了一张图过来便于理解线程池中任务执行会经过哪些判断。
添加线程池任务的入口
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();//任务为空时抛出异常
//如果线程池线程大小小于核心线程,就新建一个线程加入任务并启动线程
//如果线程池线程大小大于核心线且且添加任务到线程失败,就把任务添加到阻塞队列
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {//新建线程并启动
if (runState == RUNNING && workQueue.offer(command)) {//添加任务到队列
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);//添加到队列失败或已满,做拒接任务处理策略
}
//若阻塞队列失败或已满;这里新建一个线程并启动做应急处理(这里就是用到了maximumPoolSize参数)
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // 若线程池的线程超过了maximumPoolSize;就做拒绝处理任务策略
}
}