引入线程池的原因
通常我们需要使用线程去完成某项任务的时候都会去创建一个线程,一般都会这么写:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// TODO
}
});
thread.start();
这样操作直接且简单,当然是没有错的,但是却存在这一些问题。在应付一些线程并发不多的情况时是完全够用的,但是如果并发的线程数量很多,就会造成系统的效率降低。主要会造成如下影响:
- 频繁创建和销毁线程占用大量不必要的系统处理时间,影响性能。
- 频繁创建和销毁线程容易导致 GC 的频繁执行,造成内存抖动,导致移动设备出现卡顿。
- 大量的线程并发非常消耗内存,容易造成 OOM 问题。
- 不利于扩展,如定时执行、周期执行、线程中断。
而解决上面问题的方法,就是引入线程池的概念。线程池使得线程可以重复利用,执行完任务后并不会销毁线程,而是继续执行其他的任务。这样可以有效的减少并控制创建线程的数量,防止并发线程过多,内存消耗过度,从而提高系统的性能。
同时线程池还可以很方便的控制线程的并发数量,线程的定时任务,单线程顺序执行等等。
ExecutorService 接口
ExecutorService 就是一般所说的线程池接口,它继承 Executor 接口,同时还提供了一些管理终止的方法,以及可以跟踪一个或者多个异步任务线程执行状况并生成 Future 的方法。
而真正意义上的线程池是 ThreadPoolExecutor,它实现了 ExecutorService 接口,并且封装了一系列接口使其具有线程池的特性。
线程池:ThreadPoolExecutor
查看源码后我们发现 ThreadPoolExecutor 有四个构造方法,都是调用其中一个构造方法进行初始化操作。具体代码如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) { ... }
参数 | 作用 |
| 线程池中的核心线程数量 |
| 线程池中的最大线程数量 |
| 超过核心线程数量的多余空闲线程,在超过 keepAliveTime 时间内没有任务,则被销毁 |
| keepAliveTime 的时间单位 |
| 任务队列,用来存储已经提交但没有被执行的任务,不同的线程池采取的排队策略各不相同 |
| 线程工厂,用来创建线程池中的线程 |
| 当最大线程数和任务队列已经饱和,而导致无法接受新的任务时,用来拒绝接受任务的策略 |
五种不同功能的线程池
可以看出想要创建一个 ThreadPoolExecutor 对象并不容易,所以一般推荐使用工厂类 Executors 的工厂方法来创建线程池对象。Executors 主要提供了下面五种不同功能的线程池:
1. 固定型线程池 newFixedThreadPool
创建一个固定线程数量的线程池。每次提交一个任务就创建一个线程,直到达到设定的线程数量,之后线程池中的线程数量不再变化。当有新任务提交时,如果有空闲的线程,则由该空闲线程处理任务,否则将任务存至任务队列中,一旦有线程空闲下来,则按照 FIFO 的方式处理队列中的任务。该线程池适合一些稳定的正规并发线程,多用于服务器。
定义:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
运行实例:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("Thread: " + threadName + ", running Task" + index);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
运行结果:
02-24 08:15:20 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 08:15:20 I/System.out: Thread: pool-1-thread-2, running Task2
02-24 08:15:20 I/System.out: Thread: pool-1-thread-3, running Task3
02-24 08:15:23 I/System.out: Thread: pool-1-thread-1, running Task4
02-24 08:15:23 I/System.out: Thread: pool-1-thread-2, running Task5
02-24 08:15:23 I/System.out: Thread: pool-1-thread-3, running Task6
02-24 08:15:26 I/System.out: Thread: pool-1-thread-1, running Task7
02-24 08:15:26 I/System.out: Thread: pool-1-thread-2, running Task8
02-24 08:15:26 I/System.out: Thread: pool-1-thread-3, running Task9
02-24 08:15:29 I/System.out: Thread: pool-1-thread-1, running Task10
观察线程名发现一共只创建了 3 个线程处理任务,当所有线程都处于运行状态时,再提交的任务则会进入等待,3 个线程处理完之前任务后会被等待队列中的任务复用,所以观察时间发现,每次都是 3 个任务同时运行,间隔 3 秒后再运行后面 3 个任务,任务执行的顺序即提交的顺序。
2. 缓存型线程池 newCachedThreadPool
创建一个可以根据实际情况调整线程数量的线程池。线程池中的线程数量不确定,当需要时会创建新的线程,当有线程空闲时复用空闲线程。通常对执行大量短暂异步任务的程序,可以提升其效率。
当然,线程池中的线程并不会越来越多,每个线程都有一个参数用来设置保持活动的时间,一旦线程空闲时间超过了该时间,则立即销毁该线程,默认的保持活动时间为 60 秒,我们可以在其定义中看到这个默认参数。
定义:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
运行实例:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 1; i <= 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
final int index = i;
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("Thread: " + threadName + ", running Task" + index);
try {
Thread.sleep(index * 500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
运行结果:
02-24 08:25:29 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 08:25:30 I/System.out: Thread: pool-1-thread-1, running Task2
02-24 08:25:31 I/System.out: Thread: pool-1-thread-2, running Task3
02-24 08:25:32 I/System.out: Thread: pool-1-thread-1, running Task4
02-24 08:25:33 I/System.out: Thread: pool-1-thread-2, running Task5
02-24 08:25:34 I/System.out: Thread: pool-1-thread-1, running Task6
02-24 08:25:35 I/System.out: Thread: pool-1-thread-3, running Task7
02-24 08:25:36 I/System.out: Thread: pool-1-thread-2, running Task8
02-24 08:25:37 I/System.out: Thread: pool-1-thread-1, running Task9
02-24 08:25:38 I/System.out: Thread: pool-1-thread-4, running Task10
我们让任务间隔 1 秒提交一次,并且每个任务的时间逐渐增长,从结果可以发现,每隔 1 秒就会有一个任务被执行,刚开始一个线程可以处理过来,但随着任务时间的增长,线程池创建了新的线程来处理那些提交的任务,当有之前的线程处理完后,也会被赋予任务执行。最后一共创建了 4 个线程。
3. 单例线程 newSingleThreadExecutor
创建一个只含有一个线程的线程池。每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,当这个线程空闲时,再按照 FIFO 的方式顺序执行队列中的任务。
定义:
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
运行实例:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 1; i <= 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("Thread: " + threadName + ", running Task" + index);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
运行结果:
02-24 09:00:18 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 09:00:19 I/System.out: Thread: pool-1-thread-1, running Task2
02-24 09:00:20 I/System.out: Thread: pool-1-thread-1, running Task3
02-24 09:00:21 I/System.out: Thread: pool-1-thread-1, running Task4
02-24 09:00:22 I/System.out: Thread: pool-1-thread-1, running Task5
02-24 09:00:23 I/System.out: Thread: pool-1-thread-1, running Task6
02-24 09:00:24 I/System.out: Thread: pool-1-thread-1, running Task7
02-24 09:00:25 I/System.out: Thread: pool-1-thread-1, running Task8
02-24 09:00:26 I/System.out: Thread: pool-1-thread-1, running Task9
02-24 09:00:27 I/System.out: Thread: pool-1-thread-1, running Task10
结果显而易见,从始至终只有一个线程在执行,该线程顺序执行已提交的线程。
4. 调度型线程池 newScheduledThreadPool
创建一个可指定大小的,可以调度线程根据 schedule 延迟执行,或者周期执行的线程池。
运行实例:
System.out.println("Start Task");
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
for (int i = 1; i <= 10; i++) {
final int index = i;
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("Thread: " + threadName + ", running Task" + index);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 3, TimeUnit.SECONDS);
}
运行结果:
02-24 09:47:45 I/System.out: Start Task
02-24 09:47:48 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 09:47:48 I/System.out: Thread: pool-1-thread-2, running Task2
02-24 09:47:48 I/System.out: Thread: pool-1-thread-3, running Task3
02-24 09:47:50 I/System.out: Thread: pool-1-thread-1, running Task5
02-24 09:47:50 I/System.out: Thread: pool-1-thread-2, running Task4
02-24 09:47:50 I/System.out: Thread: pool-1-thread-3, running Task6
02-24 09:47:52 I/System.out: Thread: pool-1-thread-1, running Task7
02-24 09:47:52 I/System.out: Thread: pool-1-thread-3, running Task8
02-24 09:47:52 I/System.out: Thread: pool-1-thread-2, running Task9
02-24 09:47:54 I/System.out: Thread: pool-1-thread-1, running Task10
运行情况基本和 newFixedThreadPool 类似,但是在开始运行时,有 3 秒钟的延迟。
5. 调度型单例线程 newSingleThreadScheduledExecutor
创建一个只含有一个线程,可以调度线程根据 schedule 延迟执行,或者周期执行的线程池。
运行实例:
System.out.println("Start Task");
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
singleThreadScheduledExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("Thread: " + threadName + ", running Task1");
}
}, 1, 2, TimeUnit.SECONDS);
运行结果:
02-24 10:01:36 I/System.out: Start Task
02-24 10:01:37 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:39 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:41 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:43 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:45 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:47 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:49 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:02:51 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:02:53 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:02:55 I/System.out: Thread: pool-1-thread-1, running Task
运行情况基本和 newSingleThreadExecutor 类似,但是在开始运行时,有 1 秒钟的延迟,并且每隔 2 秒周期性的执行一次任务。
自定义线程池
如果仔细观察上面五种线程池的定义就会有所发现,其实线程池的功能不同,和其内部的 BlockingQueue 不同有关。如果我们想要实现一些不同功能的自定义线程池,可以从其中的 BlockingQueue 着手。
比如现在有一种 BlockingQueue 的实现类是 PriorityBlockingQueue,他可以实现队列按照优先级排序,那我们就可以利用它实现一个按任务的优先级来处理任务的线程池。
首先创建一个实现了 Runnable 与 Comparable 接口的抽象类 PriorityRunnable,用来存放任务。实现了 Comparable 接口就可以进行优先级的比较。
public abstract class PriorityRunnable implements Runnable, Comparable<PriorityRunnable> {
private int mPriority;
public PriorityRunnable(int priority) {
if (priority < 0 ) {
throw new IllegalArgumentException();
}
mPriority = priority;
}
@Override
public int compareTo(PriorityRunnable runnable) {
int otherPriority = runnable.getPriority();
if (mPriority < otherPriority) {
return 1;
} else if (mPriority > otherPriority) {
return -1;
} else {
return 0;
}
}
public int getPriority() {
return mPriority;
}
}
接着就可以创建一个基于 PriorityBlockingQueue 实现的线程池,其核心数量定义为 3,方便测试结果。然后创建 10 个优先级依次增加的 PriorityRunnable 任务,提交给线程池执行。
ExecutorService threadPool = new ThreadPoolExecutor(3, 3, 0L, TimeUnit.SECONDS, new PriorityBlockingQueue<Runnable>());
for (int i = 1; i <= 10; i++) {
final int priority = i;
threadPool.execute(new PriorityRunnable(priority) {
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println("Thread: " + name + ", running PriorityTask" + priority);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
运行结果:
02-25 11:34:45 I/System.out: Thread: pool-1-thread-1, running PriorityTask1
02-25 11:34:45 I/System.out: Thread: pool-1-thread-2, running PriorityTask2
02-25 11:34:45 I/System.out: Thread: pool-1-thread-3, running PriorityTask3
02-25 11:34:47 I/System.out: Thread: pool-1-thread-1, running PriorityTask10
02-25 11:34:47 I/System.out: Thread: pool-1-thread-2, running PriorityTask9
02-25 11:34:47 I/System.out: Thread: pool-1-thread-3, running PriorityTask8
02-25 11:34:49 I/System.out: Thread: pool-1-thread-1, running PriorityTask7
02-25 11:34:49 I/System.out: Thread: pool-1-thread-2, running PriorityTask6
02-25 11:34:49 I/System.out: Thread: pool-1-thread-3, running PriorityTask5
02-25 11:34:51 I/System.out: Thread: pool-1-thread-1, running PriorityTask4
可以看到,运行结果和 newFixedThreadPool 非常相似,唯一不同就在于等待任务不是按照 FIFO 的方式执行,而是优先级较高的任务先执行。
线程池的优化
线程池可以自定义内部线程的数量,定义的数量影响着系统的性能表现,如果定义过大会浪费资源,定义过小线程并发量太低,处理速度也变低了,所以合理的设置线程数量是很重要的。通常需要考虑 CPU 的核心数、内存的大小、并发请求的数量等因素。
通常核心线程数量可以设为 CPU 核心数 + 1,最大线程数量可以设为 CPU 核心数 * 2 + 1。
获取 CPU 核心数的方法为:
Runtime.getRuntime().availableProcessors();
线程池的调用方式
以下方法都可以调用线程池执行,但是效果不同,可以参考文档或源码了解:
void execute(Runnable command);
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);
<T> T invokeAny(Collection<? extends Callable<T>> tasks);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
线程池的关闭
void shutdown();
该方法不再接受新的任务提交,在终止线程池之前,允许执行完以前提交的任务。
List<Runnable> shutdownNow();
该方法不再接受新的任务提交,并且把任务队列中的任务直接移除,尝试停止正在执行的任务。返回等待的任务列表。