我们进行android开发的过程中,很多情况下都会使用到线程,例如我们会新开线程去下载图片,读取本地的文件操作等。如果每次我们都是新开线程去处理,就会导致资源的消耗过多,因为毕竟没new一个线程是会消耗一定的资源的。为了有效的解决过多线程带来的问题,android也是利用了java的线程池原理。之前也只是知道线程池可以复用已有线程的资源,只是知道会用线程池,但是不知道线程池实现的基本原理。于是利用工作的一些空闲时间研究下线程池的实现方式,由于本文自己是对android api 18的源码分析理解,可能有些地方理解的不对,希望可以跟大家一起学习。
线程池的最主要实现实在ThreadPoolExecutor 这个类中,因此,本文的重点也是对这个类的研究。在研究这个类的相关方法之前,需要对本类中的几个重要成员变量进行解释下。这里主要是参考了了
private static final int CAPACITY = (1 << COUNT_BITS) - 1;//ctl可以表示的线程数的最大值,这里就是29位可以表示的最大值了
// 下面几个都是表示线程池的状态,用ctl的高三位来表示
private static final int RUNNING = -1 << COUNT_BITS;//RUNNING状态,在该状态下线程池可以接收新的任务,也可以运行队列中的任务,它的值是 -1 右移29位
private static final int STOP = 1 << COUNT_BITS;//STOP状态,该状态下线程池不能接收新的任务,也不再执行队列中的任务,并且要中断正在处理的任务。
private static final int TIDYING = 2 << COUNT_BITS;//TIDYING状态,该状态下的线程池所有任务均已终止了,workerCount的值为0,转到TIDYING状态的线程即将要执行terminated()方法。
private static final int TERMINATED = 3 << COUNT_BITS;//该状态下的线程池说明terminated()方法执行结束
对于上面的几种状态,为什么有些不能接收新的任务,有些可以,这个我们会在后续的代码分析中进行解释。
2,介绍完上面的几个变量之后介绍两个基本方法
//返回当前线程池的状态,也就是返回ctl的高3位
private static int runStateOf(int c)
{ return c & ~CAPACITY; }
//返回当前线程池中的线程的数量,也就是ctl的低29位的值
private static int workerCountOf(int c) { return c & CAPACITY; }
3,有了上面对几个变量和基本方法的理解我们就可以开始着手线程池源码的分析了,我们在使用线程池的时候都是调用Executor.execute(Runnable command)方法开始的。由于Executor是一个接口,而ThreadPoolExecutor实现了这个接口。那我们就从ThreadPoolExecutor.excute(Runnable command)方法开始分析。为了分析方便,这里是在源码的基础上进行一些个人的理解。
public void execute(Runnable command) {
//如果command为空的话会直接抛出异常
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//获取当前的线程池中的线程数量
if (workerCountOf(c) < corePoolSize) {
//如果线程池中的线程数量少于corPoolSize的话会执行addWorker方法
if (addWorker(command, true))
return;
c = ctl.get();
}
//走到这里的话说明当前线程池中的线程的数量是>= corePoolSize的,不然的话会走前面的if逻辑的。
//如果这里workQueue(也就是我们使用的队列类型)成功的把command任务加到队列的话,就走到if里面去了
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//这里会重新判断线程池的状态,如果这时候线程池不在running状态的话,就会remove(command),也就是把command从队列中移除
//如果上面的判断成立的话就会执行reject方法,也就是会拒绝这个command,至于reject的具体实现我们后面再分析
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0) //如果线程池中的线程数为 0 的话,会通过addWorker 方法创建一个空的线程
addWorker(null, false);
}
//走到这里的话说明 1,线程池的线程数已经>=corePoolSize了 2,将command添加workQueue队列中失败了(添加到队列失败或成功,跟具体的队列也有关系,有的队列 //可以无限添加,而有的队列是不可以的) 3,,线程池不在running状态的话也会走到这里的
//这里会通过addWorker方法将command任务添加进去,如果添加失败的话也会执行reject方法
else if (!addWorker(command, false))
reject(command);
}
通过前面对execute方法的分析,我们可以总结下这个方法执行的主要作用是 1,如果当前线程池中的线程数少于corePoolSize的话就会新建一个线程去处理command任务(这时即使线程池中有空闲的线程也不会使用,而是去新建一个线程,为什么这么说?等会我们在分析addWorker 这个方法时就知道了)。2,如果当前线程池中的线程数目已经大于或等于corePoolSize了,就会尝试把command任务添加到任务队列workQueue中。3,如果在2中添加任务不成功的话就会在线程池中新建一个thread去执行command任务,也就是调用addWorker方法。
也就是说只有在队列中的任务是会复用线程池中的已有线程的,其他情况都是在线程池中新建线程。
分析完execute方法之后,我们来分析下addWorkre方法,这个方法在execute中是经常用到的
private boolean addWorker(Runnable firstTask, boolean core) {
retry:for (;;) {
//rs 表示线程池的状态
int c = ctl.get();int rs = runStateOf(c);
// Check if queue empty only if necessary.
//下面的判断可以写成( (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())))
//上面的判断要成立的话需要
// 1.rs >= SHUTDOWN 判断线程池的状态不在running
//2.re不等于SHUTDOWN 或者firstTask不为空,或者workQueue队列为空
// 对于前面的当前的线程数小于corePoolSize的情况来说,fistTask != null ,core = true; 所以在这种情况下,如果线程池是出于running状态的话,这个if的判断是为false的,继续往下走
if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
//获取当前线程池中的线程数量
int wc = workerCountOf(c);
//判断,1,如果当前的线程数大于线程数的最大值,直接返回false
// 2,根据core的值来判断,如果core = true,比较当前线程数是否大于corePoolSize。如果core = false的话,比较当前线程数是否大于maximumPoolSize的值
if (wc >= CAPACITY ||wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//如果前面的判断都过了的话,将当前线程数加一,然后跳出循环
if (compareAndIncrementWorkerCount(c))break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//新建一个worker对象
Worker w = new Worker(firstTask);
//这里的Thread t是在Worker类中定义的一个成员变量,分析Worker类的实现就知道这里thread是通过ThreadFactory new 出来的一个线程。
//这也就是为什么我们在前面说的只要走到这个方法中的command任务都会新建线程去处理,而不是利用线程池中已有的线程。
Thread t = w.thread;final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int c = ctl.get();
int rs = runStateOf(c);
//这里再次检查线程池的状态
if (t == null ||
(rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null))) {
//如果线程池已经不在running状态 或 task == null的话这里会做一些终止线程的操作,这个操作函数在后面再分析
decrementWorkerCount();tryTerminate();
return false;
}
//将新建的worker对象加到workers中,并且如果workers的大小超出之前定义的话就扩展下大小
workers.add(w);int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
} finally {
mainLock.unlock();
}
//调用thread.start 方法开启线程
t.start();
// It is possible (but unlikely) for a thread to have been
// added to workers, but not yet started, during transition to
// STOP, which could result in a rare missed interrupt,
// because Thread.interrupt is not guaranteed to have any effect
// on a non-yet-started Thread (see Thread#interrupt).
//线程启动之后又会再次判断下线程池的状态,如果这时候线程池出于STOP状态,而线程又没有interrupt的话会尝试着去interrupt这个线程。
if (runStateOf(ctl.get()) == STOP && ! t.isInterrupted())t.interrupt();
return true;
}
通过分析可以知道,其实在addWorker方法中最主要的作用就是为command任务新建线程,然后启动这个线程。
分析到这里为止我们知道了线程池在什么情况下会新建线程,但是还不知道线程池是如何服用池中已有的线程的。这个关键的实现就是我们下面要分析的内容。
在上面的addWorker()方法中分析了如果可以新建线程,就会建一个thread,然后启动这个thread,我们知道,thread.start 其实调用的是thread()对应的run方法。那这里的run是怎么执行的呢。我们来看下这个thread是如何创建的。这就要看Worker类的实现了。其实这里的Worker类只是对Runnable进行了一些包装而已,Worker类的源码也是在ThreadPoolExecutor这个类中,是ThreadPoolExecutor的一个内部类。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
final Thread thread;
Runnable firstTask;
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
this.firstTask = firstTask;
//这里可以看出这里的thread对应的Runnable接口就是Worker类里实现的Runnable接口
//因此前面分析的thread.start(),其实会调用这里的run方法
this.thread = getThreadFactory().newThread(this);
}
//这里是正真的线程执行的方法,这里调用了本地的一个runWorker方法。这个方法会在下面介绍
public void run() {
runWorker(this);
}
protected boolean isHeldExclusively() {
return getState() == 1;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
}
从上面的讲解我们知道线程的执行最后会调用runWorker方法,接下来我们就分析这个方法的实现,在这个方法中也就体现了线程池是如何复用池中已有的线程的。
final void runWorker(Worker w) {
//这里的task其实就是我们最早创建线程是传的command任务,也就是我们想要线程为我们做的事情,比如下载文件等等
Runnable task = w.firstTask;
w.firstTask = null;
//该变量用来表示线程是否异常终止,后面会根据实际情况修改这个值
boolean completedAbruptly = true;
try {
//这里task不为空,或者getTask不为空的话就执行下面的操作,
//这里使用的是一个while循环,也就是说,当我们新开线程去处理一个command请求时,如果这时候又有其他的请求来了,通过前面的分析可以知道这些请求有可能是 //会被放到队列去的,如果这些请求成功的被放到队列去了,那么当这个新开的线程执行完它本身的任务时,就会通过getTask,从队列中去取任务,如果取到的任务不为 //空,就会继续执行这个从队列中取出的任务。也就是说放在队列里的任务是不需要新开线程处理的,线程池会用已有的线程去处理这些任务。从而达到节省线程资源的 //目的。
while (task != null || (task = getTask()) != null) {
w.lock();
clearInterruptsForTaskRun();
try {
beforeExecute(w.thread, task);
Throwable thrown = null;
//执行线程的run方法
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
//
task = null;
//task正常执行完毕,这里讲w的完成task数加1
w.completedTasks++;
w.unlock();
}
}//这里是一个循环,也就是这里如果一个task执行完了之后会去gettask,看是否还有task执行,如果有责执行getTask得到的task
completedAbruptly = false;//走到这里说明线程没有异常,把这个值置为false
} finally {
processWorkerExit(w, completedAbruptly);//结束该线程,这个结束线程的方法我们就不去一一分析了。
}
}
前面的分析过程中我们知道了线程池会在什么样的情况下建立新的线程,又会在什么样的情况下利用已有的线程。最后,这里附上jdk1.7文档中对ThreadPoolExecute类的一些说明,以便可以更好的理解上面分析的一些内容
一个 ExecutorService
,它使用可能的几个池线程之一执行每个提交的任务,通常使用 Executors
工厂方法配置。
线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。
为了便于跨大量上下文使用,此类提供了很多可调整的参数和扩展钩子 (hook)。但是,强烈建议程序员使用较为方便的 Executors
工厂方法Executors.newCachedThreadPool()
(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)
(固定大小线程池)和Executors.newSingleThreadExecutor()
(单个后台线程),它们均为大多数使用场景预定义了设置。否则,在手动配置和调整此类时,使用以下指导:
-
核心和最大池大小
-
ThreadPoolExecutor 将根据 corePoolSize(参见
getCorePoolSize()
)和 maximumPoolSize(参见getMaximumPoolSize()
)设置的边界自动调整池大小。当新任务在方法execute(java.lang.Runnable)
中提交时,如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程。如果设置的 corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。在大多数情况下,核心和最大池大小仅基于构造来设置,不过也可以使用setCorePoolSize(int)
和setMaximumPoolSize(int)
进行动态更改。
按需构造
-
默认情况下,即使核心线程最初只是在新任务到达时才创建和启动的,也可以使用方法
prestartCoreThread()
或prestartAllCoreThreads()
对其进行动态重写。如果构造带有非空队列的池,则可能希望预先启动线程。
创建新线程
-
使用
ThreadFactory
创建新线程。如果没有另外说明,则在同一个ThreadGroup
中一律使用Executors.defaultThreadFactory()
创建线程,并且这些线程具有相同的 NORM_PRIORITY 优先级和非守护进程状态。通过提供不同的 ThreadFactory,可以改变线程的名称、线程组、优先级、守护进程状态,等等。如果从 newThread 返回 null 时 ThreadFactory 未能创建线程,则执行程序将继续运行,但不能执行任何任务。
保持活动时间
-
如果池中当前有多于 corePoolSize 的线程,则这些多出的线程在空闲时间超过 keepAliveTime 时将会终止(参见
getKeepAliveTime(java.util.concurrent.TimeUnit)
)。这提供了当池处于非活动状态时减少资源消耗的方法。如果池后来变得更为活动,则可以创建新的线程。也可以使用方法setKeepAliveTime(long, java.util.concurrent.TimeUnit)
动态地更改此参数。使用 Long.MAX_VALUETimeUnit.NANOSECONDS
的值在关闭前有效地从以前的终止状态禁用空闲线程。默认情况下,保持活动策略只在有多于 corePoolSizeThreads 的线程时应用。但是只要 keepAliveTime 值非 0,allowCoreThreadTimeOut(boolean)
方法也可将此超时策略应用于核心线程。
排队
-
所有
BlockingQueue
都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:- 如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
- 如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
- 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
- 直接提交。工作队列的默认选项是
SynchronousQueue
,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。 - 无界队列。使用无界队列(例如,不具有预定义容量的
LinkedBlockingQueue
)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。 - 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如
ArrayBlockingQueue
)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
被拒绝的任务
-
当 Executor 已经关闭,并且 Executor 将有限边界用于最大线程和工作队列容量,且已经饱和时,在方法
execute(java.lang.Runnable)
中提交的新任务将被 拒绝。在以上两种情况下, execute 方法都将调用其RejectedExecutionHandler
的RejectedExecutionHandler.rejectedExecution(java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor)
方法。下面提供了四种预定义的处理程序策略:- 在默认的
ThreadPoolExecutor.AbortPolicy
中,处理程序遭到拒绝将抛出运行时RejectedExecutionException
。 - 在
ThreadPoolExecutor.CallerRunsPolicy
中,线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。 - 在
ThreadPoolExecutor.DiscardPolicy
中,不能执行的任务将被删除。 - 在
ThreadPoolExecutor.DiscardOldestPolicy
中,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。
RejectedExecutionHandler
类也是可能的,但这样做需要非常小心,尤其是当策略仅用于特定容量或排队策略时。
钩子 (hook) 方法
- 在默认的
-
此类提供 protected 可重写的
beforeExecute(java.lang.Thread, java.lang.Runnable)
和afterExecute(java.lang.Runnable, java.lang.Throwable)
方法,这两种方法分别在执行每个任务之前和之后调用。它们可用于操纵执行环境;例如,重新初始化 ThreadLocal、搜集统计信息或添加日志条目。此外,还可以重写方法terminated()
来执行 Executor 完全终止后需要完成的所有特殊处理。如果钩子 (hook) 或回调方法抛出异常,则内部辅助线程将依次失败并突然终止。
队列维护
-
方法
getQueue()
允许出于监控和调试目的而访问工作队列。强烈反对出于其他任何目的而使用此方法。remove(java.lang.Runnable)
和purge()
这两种方法可用于在取消大量已排队任务时帮助进行存储回收。
终止
-
程序 AND 不再引用的池没有剩余线程会自动 shutdown。如果希望确保回收取消引用的池(即使用户忘记调用
shutdown()
),则必须安排未使用的线程最终终止:设置适当保持活动时间,使用 0 核心线程的下边界和/或设置allowCoreThreadTimeOut(boolean)
。