线程池优点
提到线程池就必须先说一下线程池的优点,线程池的优点可以概括为以下四点:
* 重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销;
* 线程池旨在线程的复用,就避免了创建线程和销毁线程所带来的时间消耗,减少线程频繁调度的开销,从而节约系统资源,提高系统吞吐量;
* 能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象;
* 能够对线程进行简单的管理,并提供定时执行以及指定时间间隔循环执行等功能。
线程池的创建
Android 中的线程池的概念来源于 Java 中的 Executor ,Executor 是一个接口,真正的线程池的实现为 ThreadPoolExecutor,ThreadPoolExecutor 提供了一些列的参数来配置线程池,通过不同的参数可以创建功能特性不同的线程池。我们要创建一个线程池只需要 new ThreadPoolExecutor(…) 就可以创建一个线程池,但我们如此创建线程池的话,需要配置一些参数,非常麻烦,同时 Google 官方也不推荐使用这种方式来创建线程池,而是推荐使用 Executors 的工厂方法来创建线程池。但 Executors 的工厂方法创建的线程池也是直接或间接通过配置 ThreadPoolExecutor 参数来实现的,因此有必要先了解 ThreadPoolExecutor 的配置参数。
ThreadPoolExecutor
ThreadPoolExecutor 的构造方法提供了一些列的参数来配置线程池,先来了解一下 ThreadPoolExecutor 的构造方法中各个参数的含义,这些参数将直接影响到线程池的功能特性。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {...
}
corePoolSize 线程池的核心线程数,默认情况下,核心线程会在线程池中一直存活,即使它们处于闲置状态。但如果将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设置为 true ,那么闲置的核心线程在等待新任务到来时会有超时策略,这个时间间隔是由 keepAliveTime 所指定,当等待时间超过 keepAliveTime 所指定的时长后,核心线程就会被终止。
maximumPoolSize 线程池所能容纳的最大线程数,当活动线程数达到这个数值后,后续的新任务将会被阻塞。
keepAliveTime 非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收。当 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设置为 true 时,keepAliveTime 同样会作用于核心线程。
unit 用于指定 keepAliveTime 参数的时间单位,这是一个枚举,常用的有 TimeUnit .MILLISECONDS 和 TimeUnit .SECONDS。
workQueue 线程池中的任务队列,通过线程池的 execute 方法提交的 Runnable 对象会存储在这个参数中。
threadFactory 线程工厂,为线程池提供创建新的线程的功能。threadFactory 是一个接口,它只有一个方法: public abstract Thread newThread (Runnable r);
RejectedExecutionHandler 通常叫做拒绝策略,在线程池已经关闭的情况下 ,或者任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务 。在上面两种情况下,只要满足其中一种时,在使用 execute() 来提交新的任务时将会拒绝,而默认的拒绝策略是抛一个 RejectedExecutionException 异常。
ThreadPoolExecutor 执行任务时策略
以下用 currentSize 表示线程池中当前线程数量
1. 当 currentSize < corePoolSize 时,将会直接启动一个核心线程来执行任务。
2. 当 currentSize >= corePoolSize ,那么任务会被插入到任务队列中排队等待执行。
3. 如果任务队列已满,但 currentSize < maximumPoolSize,那么会立刻启动一个非核心线程来执行任务
4. 如果任务队列已满,并且 currentSize > maximumPoolSize,那么就拒绝执行任务,ThreadPoolExecutor 会调用 RejectedExecutionHandler 的 rejectedExecution 方法来通知调用者。
下面通过 demo 来验证一下:
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 30, 1, TimeUnit.SECONDS, new LinkedBlockingQueue(128));
for (int i = 0; i < 30; i++) {
final int index = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
String threadName = Thread.currentThread().getName();
Log.v(TAG, "线程:"+threadName+",正在执行第" + index + "个任务");
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executor.execute(runnable);
}
在示例中,核心线程数为 3,workQueue 的大小为 128,所以我们的线程的执行应该是先启动三个核心线程来执行任务,剩余的 27 个任务全部存在workQueue 中,等待核心线程空余出来之后执行。
那我把构造 ThreadPoolExecutor 的参数修改一下,来验证一下我们上面的结论正确与否。
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 30, 1, TimeUnit.SECONDS, new LinkedBlockingQueue(6));
for (int i = 0; i < 30; i++) {
final int index = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
String threadName = Thread.currentThread().getName();
Log.v(TAG, "线程:"+threadName+",正在执行第" + index + "个任务");
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executor.execute(runnable);
}
首先打印出来 0,1,2 说明往核心线程添加了三个任务,然后将 3,4,5,6,7,8 六个任务添加到了任务队列中,接下来要添加的任务因为核心线程已满,队列已满所以就直接开一个非核心线程来执行,因此后添加的任务反而会先执行(3,4,5,6,7,8都在队列中等着),所以我们看到的打印结果是先是 0~2,然后 9~29,然后 3~8,当然,我们在实际开发中不会这样来配置最大线程数和线程队列。
线程池的分类
由于 Google 官方不推荐使用 new 的方式来创建线程池,而是推荐使用 Executors 的工厂方法来创建线程池,通过直接或间接的配置 ThreadPoolExecutor 的参数来构建线程池,常用的线程池有如下 4 种,newFixedThreadPool ,newCachedThreadPool,
newScheduledThreadPool 和 newSingleThreadExecutor。
FixedThreadPool 通过 Executors .newFixedThreadPool 来创建。它是一个线程数量固定的线程池,当线程处于空闲状态时,它们并不会被回收,除非线程池被关闭了。当所有的线程都处于活动状态时,新任务都会处于等待状态,直到有线程空闲出来。由于 FixedThreadPool 只有核心线程并且这些核心线程不会被回收,这意味着它能快速的响应外界的请求。 newFixedThreadPool 方法的实现如下,可以发现 FixedThreadPool 中只有核心线程并且这些核心线程没有超时机制,另外任务队列也是没有大小限制。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
CachedThreadPool 通过 Executors.newCachedThreadPool 来创建,它是一个线程数量不定的线程池,它只有非核心线程,并且最大线程数为 Integer.MAX_VALUE ,由于 Integer.MAX_VALUE 是一个很大的数,实际上就相当于最大核心线程数可以任意大。当线程池中的线程都处于活动状态时,当有新任务过来时,线程池就会创建新的线程来处理新任务,否则就会利用空闲的线程来处理。线程池中的空闲线程都有超时机制,这个超时时长为 60 秒,超过 60 秒闲置线程就会被回收。SynchronousQueue 队列相当于一个空队列,这就导致任何任务都会立即执行。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
ScheduledThreadPool 通过 Executors.newScheduledThreadPool 来创建,它的核心数量是固定的,非核心线程没有限制,并且当非核心线程闲置时会被立即回收,ScheduledThreadPool 这类线程池主要用于执行定时任务和具有固定周期的重复任务。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
SingleThreadExecutor 通过 Executors.newSingleThreadExecutor 来创建。这类线程池内部只有一个核心线程,它确保所有的任务都在同一个线程中按顺序执行。SingleThreadExecutor 的意义在于统一所有的外界任务到一个线程中,这使得这些任务之间不需要处理线程同步的问题。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
线程池 ThreadPoolExecutor 的使用
使用线程池,其中涉及到一个极其重要的方法,即:
execute(Runnable command)
Executes the given task sometime in the future.
该方法意为执行给定的任务,该任务处理可能在新的线程、已入池的线程或者正调用的线程,这由 ThreadPoolExecutor 的实现决定。
newFixedThreadPool 创建一个固定线程数量的线程池,示例为:
@Override
public void onClick(View v) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v(TAG, "线程:"+threadName+",正在执行第" + index + "个任务");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
在上述代码中,创建了线程数量固定为 3 的线程池,该线程池支持的最大线程也为 3 ,而我们创建了 10 个任务让它处理,通过 log 分析,执行的情况是先执行前 3 个任务,后面 7 个任务都进入任务队列进行等待,执行完前 3 个任务后,再通过 FIFO 的方式从任务队列中取任务执行,直到所有任务都执行完毕。
newSingleThreadExecutor 创建一个只有一个线程数的线程池。通过 log 分析,每次线程池只执行一个任务,其余的任务都将进入任务队列进行等待。
@Override
public void onClick(View v) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v(TAG, "线程:"+threadName+",正在执行第" + index + "个任务");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
代码改动不大,只是改动了 ThreadPoolExecutor 的实现方式,任务都是一个一个的执行,并且都是同一个线程。
newCachedThreadPool 创建一个可以根据实际情况调整线程池中线程数量的线程池
@Override
public void onClick(View v) {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v(TAG, "线程:"+threadName+",正在执行第" + index + "个任务");
try {
long time = index * 500;
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
为了体现该线程池可以自动根据实际情况进行线程的重用,而不是一味的创建新的线程去处理任务,我设置了每隔 1s 去提交一个新任务,这个新任务执行的时间也是动态变化的。
通过 log 可以看出,新增的任务都会被立即执行。
newScheduledThreadPool 创建一个可以定时或周期性执行任务的线程池。
@Override
public void onClick(View v) {
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3);
threadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v(TAG, "线程:" + threadName + ",正在执行");
}
}, 2, 1, TimeUnit.SECONDS);
}
延迟 2 秒后,每隔 1 秒执行一次该任务
从 log 日志可以看出 ScheduledThreadPool 是 4 个线程池里面唯一一个个有延迟执行和周期重复执行的线程池。
总结
1. FixedThreadPool 只有核心线程,并且数量是固定的,也不会被回收,能更快地响应外界请求。
2. SingleThreadPool 只有一个核心线程,确保所有任务都在同一线程中按顺序完成。因此不需要处理线程同步的问题。
3. CachedThreadPool 只有非核心线程,最大线程数非常大,所有线程都活动时,会为新任务创建新线程,否则利用空闲线程处理任务,任何任务都会被立即执行。
4. ScheduledThreadPool 核心线程数固定,非核心线程数没有限制,主要用于执行定时任务以及有固定周期的重复任务。
好了,关于线程池方面的内容今天就分析到这里,在这篇文章中我们主要学习了线程池的基本概念和基本用法。下一篇文章当中,将会继续带着大家学习线程池的高级用法,讲一讲自定义线程池的知识,敬请期待。
Android 线程池高级使用