一、前言
在Android应用开发中,多线程编程被广泛应用。其中,ThreadPoolExecutor、AsyncTask、IntentService、HandlerThread和AsyncTaskLoader等是应用较多的实现方式。为了更详细地分析每一种实现方式,本文将单独对它们进行分析。后续篇章中可能会涉及到线程池的知识,因此特此本篇分析为何使用线程池,如何使用线程池以及线程池的使用原理。
二、Thread Pool基础
进程代表一个运行中的程序,一个运行中的Android应用程序就是一个进程。从操作系统的角度来说,线程是进程中可以独立执行的子任务。一个进程可以有多个线程,同一个进程中的线程可以共享进程中的资源。从JVM的角度来说,线程是进程中的一个组件,是执行Java代码的最小单位。
在Android应用开发过程中,如果需要处理异步或并发任务时可以使用线程池。使用线程池可以带来以下好处:
1. 降低资源消耗:线程的创建和销毁都需要消耗资源,重复利用线程可以避免过度消耗资源。
2. 提高响应速度:当有任务需要执行时,可以不用重新创建线程就能开始执行任务。
3. 提高线程的管理性:过多的创建线程会降低系统的稳定性,使用线程池可以统一分配、调优和监控。
Thread Pool模式的原理是使用队列对待处理的任务进行缓存,并复用一定数量的工作者线程从队列中取出任务来执行。其本质是使用有限的资源来处理无限的任务。
Thread Pool模式最核心的类是ThreadPoolExecutor,它是线程池的实现类。使用Executors可以创建三种类型的ThreadPoolExecutor类,主要包括以下三种类型:
1. FixedThreadPool:固定大小的线程池,可以预先指定线程的数量。
2. SingleThreadExecutor:只有一个工作线程的线程池,它保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
3. CachedThreadPool:可缓存等待执行的任务队列,如果当前线程池的大小超过处理需求,那么空闲的线程会被回收并且保留在队列中等待下一次任务的到来。
三、Executor框架分析
Executor接口提供了一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。
Executor框架中主要包括以下几个类:
1. ThreadPoolExecutor:线程池的实现类。
2. ScheduledThreadPoolExecutor:定时任务的实现类。
3. Future接口:表示异步计算的结果。
4. Runable接口:表示可以由线程执行的任务。
5. Callable接口:表示可以返回结果并由线程执行的任务。
6. Executors工具类:提供静态方法用于创建不同类型的Executor实例。
首先,主线程创建任务:任务的对象可以通过实现Runnable接口或者Callable接口实现。而Runnable可以通过Executors.callable(Runnable runnable)或者Executors.callable(Runnable runnable, Object result)方法来转换封装为Callable对象。
接下来是执行任务:执行任务的方式有两种,一种是execut()方法,执行提交的Runnable对象,ExecutorService.execute(Runnable runnable);另外一种是submit()方法,可以执行提交的Runnable对象,ExecutorService.submit(Runnable runnable),或者是执行提交的Callable对象,ExecutorService.submit(Callable callable)。
然后是取消任务:可以选择使用FutureTask.cancel(boolean flag)取消任务。
最后是关闭 ExecutorService:这将导致其拒绝新任务。有两种方式来关闭 ExecutorService。shutdown() 方法在终止前允许执行以前提交的任务,而 shutdownNow() 方法阻止等待任务启动并试图停止当前正在执行的任务。在终止时,执行程序没有任务在执行,也没有任务在等待执行,并且无法提交新任务。注意:应该关闭未使用的 ExecutorService 以允许回收其资源。下列方法分两个阶段关闭 ExecutorService。第一阶段调用 shutdown 拒绝传入任务,然后调用 shutdownNow(如有必要)取消所有遗留的任务:
四、ThreadPoolExecutor原理分析
当向一个线程池中添加一个任务时,线程池是如何工作的?下面根据源代码来分析其中的原理:
以下是线程池的主要处理流程图:
从图中可以看出,当提交一个任务时,首先判断核心线程池是否都在执行任务,如果还有未执行任务的线程,则会新创建一个核心线程来执行此任务,否则,将进入下一个流程当中。如果存储任务的队列没有满,那么任务则会存储到这个队列当中,如果该队列已经满了,则会进入下一个流程当中。
线程池是一种多线程处理形式,它的主要优点在于可以减少为每个任务创建和销毁线程的开销。当任务数量很大时,使用线程池可以提高系统的性能。
线程池中的非核心线程处于空闲状态,如果没有任务需要执行,则不会创建新线程。当有任务需要执行时,如果线程池中的核心线程数小于基本大小值,或者是存在核心线程处于空闲状态,则会创建一个线程来执行提交的任务。当线程数量等于基本大小时就不会创建了。
下面是一个简单的Java代码示例,演示如何通过ThreadPoolExecutor类创建一个线程池:
```java
// 导入所需的包
import java.util.concurrent.*;
public class ThreadPoolExecutorExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个固定大小的线程池
int corePoolSize = 3;
int maximumPoolSize = 5;
long keepAliveTime = 60L;
// 创建一个具有阻塞功能的线程池,其中队列是无界队列,用于保存等待的任务
BlockingQueue
// 创建一个工厂对象,用于创建线程
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 创建一个具有指定属性的线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue, threadFactory);
// 提交10个任务给线程池执行
for (int i = 0; i < 10; i++) {
final int taskId = i;
threadPool.execute(() -> System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName()));
}
// 关闭线程池
threadPool.shutdown();
}
}
```
. defaultHandler(饱和策略的处理模式):当队列和线程池都满的时候,这种状态就是处于饱和状态了,那么必须采取一种策略来处理不能执行的新任务。关于饱和策略的处理有四种方式:
- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:直接丢弃任务。
五、FixedThreadPool原理分析
FixedThreadPool通过Executors中的newFixedThreadPool()方法来创建的。这是一种创建可重用固定线程数量的线程池的方式,以共享的无界队列方式来运行这些线程。在这个线程池中只有核心线程,不存在非核心线程,当核心线程处于空闲状态时,线程不会被回收,只有当线程池关闭时才会回收。
如果运行的线程数少于corePoolSize,则会创建新的线程来执行任务。当有任务来不及给线程来处理时,则会将任务添加到任务队列中。当任务数少于线程数的时候,线程池的执行结果如下:
```plaintext
运行结果:
pool-1-thread-2 1
pool-1-thread-1 0
```
这说明线程池中先只创建两个线程。
如果任务数量比较多的时候,超过核心线程数,那么当线程执行完任务后会从队列中取任务执行。以下是实例代码:
```plaintext
运行结果:
pool-1-thread-1 0
pool-1-thread-4 3
pool-1-thread-3 2
pool-1-thread-2 1
pool-1-thread-4 4
pool-1-thread-3 5
pool-1-thread-2 7
pool-1-thread-1 6
pool-1-thread-4 8
pool-1-thread-3 9
pool-1-thread-2 10
pool-1-thread-1 11
pool-1-thread-4 12
...
```
从运行结果可以知道,线程池中的线程不会无限创建,数量最多为corePoolSize大小。
SingleThreadExecutor是通过使用Executors的newSingleThreadExecutor方法来创建的,以无界队列方式来运行该线程。这个线程池中内部只有一个核心线程,而且没有非核心线程。以下是SingleThreadExecutor的源代码实现:
```java
public class SingleThreadExecutor extends ThreadPoolExecutor {
public SingleThreadExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue
}
}
```
其中corePoolSize和maximumPoolSize被设置为1,其它的与FixPoolThread相同。
(1)当线程池中的线程数少于corePoolSize,则会创建一个新线程来执行任务。
(2)如果任务数量比较多的时候,超过核心线程数,那么当线程执行完任务后会从队列中取任务执行,并且按照顺序执行。
实例代码如下:
```java
public class TestSingleThreadExecutor {
public static void main(String[] args) throws InterruptedException {
SingleThreadExecutor executor = new SingleThreadExecutor(1);
for (int i = 0; i < 20; i++) {
executor.execute(new Task());
}
executor.shutdown();
}
}
class Task implements Runnable {
private int index;
public Task() {
this.index = index++;
}
@Override
public void run() {
System.out.println("pool-" + Thread.currentThread().getName() + "-thread-" + index % 15 + " " + index);
}
}
```
运行结果:
```
pool-main-thread-0 pool-main-thread-1 pool-main-thread-2 pool-main-thread-3 pool-main-thread-4 pool-main-thread-5 pool-main-thread-6 pool-main-thread-7 pool-main-thread-8 pool-main-thread-9 pool-main-thread-10 pool-main-thread-11 pool-main-thread-12 pool-main-thread-13 pool-main-thread-14 pool-main-thread-15 pool-main-thread-16 pool-main-thread-17 pool-main-thread-18 pool-main-thread-19 pool-main-thread-20
pool-main-thread-0 0
pool-main-thread-1 1
pool-main-thread-2 2
pool-main-thread-3 3
pool-main-thread-4 4
pool-main-thread-5 5
pool-main-thread-6 6
pool-main-thread-7 7
pool-main-thread
线程池中的keepAliveTime属性表示了空闲线程在被回收之前,可以等待新任务的最长时间。当线程的空闲时间超过这个值时,线程将被终止并回收。CachedThreadPool使用的队列类型是SynchronousQueue,这是一个无界队列,即它的容量是无限的。
(1) 如果线程池中的线程处理任务的速度小于提交任务的速度,线程池会不断地创建新线程来处理任务。然而,过多的创建线程可能会耗尽CPU和内存资源。
(2) 当线程池中的所有线程都处于活动状态时,线程池会创建新的线程来处理主线程提交的任务。如果有空闲线程,则会利用这些空闲线程来处理任务。
(3) SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作。CachedThreadPool使用SynchronousQueue,将主线程提交的任务传递给空闲线程执行。
八、总结:熟悉了线程池的一些相关知识后,我们需要了解如何合理配置线程池以及如何选择合适的线程池方式。为了合理配置线程池,需要从任务的性质、优先级和依赖性等多个方面进行分析。根据任务的性质(CPU密集型、IO密集型或混合型),可以选择适当的线程数量;根据任务的优先级,可以使用PriorityBlockingQueue进行处理;根据任务的执行时间差异,可以将任务分配给不同规模的线程池或优先级队列。此外,还需要考虑任务是否依赖其他系统资源(如数据库连接)。
Java中线程池有多种实现方式,其中最常见的包括以下三种:FixedThreadPool、SingleThreadExecutor和CachedThreadPool。
- FixedThreadPool适合比较耗资源的任务。
- SingleThreadExecutor适合按照顺序执行的任务。
- CachedThreadPool适合执行大量耗时较少的任务。