线程池的由来:由于业务需求,我们需要多个线程同时执行(也叫并发执行),我们会去创建多个线程来执行任务。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。因此线程池就诞生了,线程池中的线程会在任务执行完毕后不会被立即销毁,而是被重复利用,避免创建新的线程。在java中,有专门一个类用来创建线程池并使用线程池。
ThreadPoolExecutor.java
ThreadPoolExecutor有四个构造方法,最终都是由下面这个来实现
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
其中的几个重要参数的含义如下:
- corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
- maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:the time unit for the keepAliveTime args
- workQueue:the queue to use for holding tasks before they are excuted and only hold the Runnable tasks submited by execute() method
- threadFactory:线程工厂,主要用来创建线程;
- handler: the handler to use when execution is blocked because the thread bounds and queue capacities are reached
ThreadPoolExecutor类中的几个重要的方法:
- execute(Runnable task)。我们经常调用的submit()方法来开始任务,其内部实现也还是execute()方法。从注释可以看出,该方法里面大概走了三部
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.*/
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
如果此时正在运行的线程数量少于核心线程数量,我们将开启一个新的线程,并将runnable参数作为这个线程的第一个要执行的任务。addWorker中将自动检查是否应该添加新线程。如果添加成功,将直接返回,否则重新获取此时的线程运行状态。
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.*/
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))//线程挂了,将对应的任务给停下来
reject(command);
else if (workerCountOf(recheck) == 0)//线程池被清空了,重新添加
addWorker(null, false);
}
一旦新线程成功添加到workQueue,将会进行二次检测,检测新添线程运行状态。因为有可能新的线程由于种种原因出现错误状态。
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.*/
else if (!addWorker(command, false))
reject(command);
如果我们添加线程失败,线程没有添加到workQueue中,我们将尝试新创建一个线程,如果还是创建失败,我们将会会reject(command),也就是线程池处于shutdown,我们也会reject任务
一定要弄清楚下面两个概念
//任务队列
private final BlockingQueue workQueue;
//作业线程集合
private final HashSet workers = new HashSet();
在创建ThreadPoolExecutor(ExecutorService)线程池时,我们一般可以用工厂方法来创建:
Executors.newFixedThreadPool(int workQueue size not poolsize/threadnum );
我们可以认为:workqueue为任务队列大小,workers大小和poolsize一样大。默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。线程池大小大于workers数量时,不需要添加到workqueue中;一但超过corePoolSize,则添加到queue中。如果添加到queue中失败,就会再创建新的thread,但是要确保thread数量小于maxCorePoolSize。值得注意的是,添加任务时,创建线程则要start()任务,添加到queue中时不需要主动执行任务。这是因为在worker工作的时候会不断去读取queue中的任务。
而之所以线程执行完后不会退出的原因是在Worker的run()方法中会去不断地getTask(),这个方法是阻塞的
Runnable getTask() {
for (;;) {
try {
int state = runState;
if (state > SHUTDOWN)
return null;
Runnable r;
if (state == SHUTDOWN) // Help drain queue
r = workQueue.poll();
else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //如果线程数大于核心池大小或者允许为核心池线程设置空闲时间,
//则通过poll取任务,若等待一定的时间取不到任务,则返回null
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);//阻塞
else
r = workQueue.take();
if (r != null)
return r;
if (workerCanExit()) { //如果没取到任务,即r为null,则判断当前的worker是否可以退出
if (runState >= SHUTDOWN) // Wake up others
interruptIdleWorkers(); //中断处于空闲状态的worker
return null;
}
// Else retry
} catch (InterruptedException ie) {
// On interruption, re-check runState
}
}
}
如果取到了task就去执行runTask()
private void runTask(Runnable task) {
final ReentrantLock runLock = this.runLock;
runLock.lock();
try {
if (runState < STOP &&
Thread.interrupted() &&
runState >= STOP)
boolean ran = false;
beforeExecute(thread, task); //beforeExecute方法是ThreadPoolExecutor类的一个方法,没有具体实现,用户可以根据
//自己需要重载这个方法和后面的afterExecute方法来进行一些统计信息,比如某个任务的执行时间等
try {
task.run();
ran = true;
afterExecute(task, null);
++completedTasks;
} catch (RuntimeException ex) {
if (!ran)
afterExecute(task, ex);
throw ex;
}
} finally {
runLock.unlock();
}
}
这样就是在runnable中执行runnable()。一直这样,知道我们让它退出,或者timeout,或者我们设置了允许线程自己退出。这只是一个Worker,其他worker也是这样的,一个worker对应一个Thread,它们同时操作mworkQueue。不需要另外的分发线程DispatchThread(Volley开源库就是在工作thread基础上增加了分发线程用来管理工作线程),自己让自己poll。妙不可言!!!
JAVA中线程池的种类
注意:下面几种线程类型都是通过Executors的方法创建出来的,方法名称对应下面的线程类型,他们的实际对象还是ThreadPoolExecutor
newFixedThreadPool
建立一个线程数量固定的线程池,规定的最大线程数量,超过这个数量之后进来的任务,会放到等待队列中,如果有空闲线程,则在等待队列中获取,遵循先进先出原则。
创建固定线程数量线程池, corePoolSize 和 maximumPoolSize 要一致,即核心线程数和最大线程数(核心+非核心线程)一致,Executors 默认使用的是 LinkedBlockingQueue 作为等待队列,这是一个无界队列,这也是使用它的风险所在,除非你能保证提交的任务不会无节制的增长,否则不要使用无界队列,这样有可能造成等待队列无限增加,造成 OOM。
newSingleThreadExecutor
建立一个只有一个线程的线程池,如果有超过一个任务进来,只有一个可以执行,其余的都会放到等待队列中,如果有空闲线程,则在等待队列中获取,遵循先进先出原则。使用 LinkedBlockingQueue 作为等待队列。
这个方法同样存在等待队列无限长的问题,容易造成 OOM,所以正确的创建方式参考上面固定数量线程池创建的方式,只是把 poolSize 设置为 1
newCachedThreadPool
缓存型线程池,在核心线程达到最大值之前,有任务进来就会创建新的核心线程,并加入核心线程池,即时有空闲的线程,也不会复用。达到最大核心线程数后,新任务进来,如果有空闲线程,则直接拿来使用,如果没有空闲线程,则新建临时线程。并且线程的允许空闲时间都很短,如果超过空闲时间没有活动,则销毁临时线程。关键点就在于它使用 SynchronousQueue 作为等待队列,它不会保留任务,新任务进来后,直接创建临时线程处理,这样一来,也就容易造成无限制的创建线程,造成 OOM
newScheduledThreadPool
计划型线程池,可以设置固定时间的延时或者定期执行任务,同样是看线程池中有没有空闲线程,如果有,直接拿来使用,如果没有,则新建线程加入池。使用的是 DelayedWorkQueue 作为等待队列,这中类型的队列会保证只有到了指定的延时时间,才会执行任务。