本文译自:Java Concurrency – Part 7 : Executors and thread pools
让我们开始来从入门了解一下 Java 的并发编程。
本文主要介绍如何开始创建线程以及管理线程池,在 Java 语言中,一个最简单的线程如下代码所示:
Runnable runnable = new Runnable(){ public void run(){ System.out.println("Run"); } }
可通过下面一行代码来启动这个线程:
new Thread(runnable).start();
这是一个再简单不过的例子了,但如果你有许多需要长时间运行的任务同时执行,并需要等所有的这些线程都执行完毕,还想得到一个返回值,那么这就有点小小难度了。但 Java 已经有解决方案给你,那就是 Executors ,一个简单的类可以让你创建线程池和线程工厂。
一个线程池使用类 ExecutorService 的实例来表示,通过 ExecutorService 你可以提交任务,并进行调度执行。下面列举一些你可以通过 Executors 类来创建的线程池的类型:
- Single Thread Executor : 只有一个线程的线程池,因此所有提交的任务是顺序执行,代码:Executors.newSingleThreadExecutor()
- Cached Thread Pool : 线程池里有很多线程需要同时执行,老的可用线程将被新的任务触发重新执行,如果线程超过60秒内没执行,那么将被终止并从池中删除,代码:Executors.newCachedThreadPool()
- Fixed Thread Pool : 拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,代码:Executors.newFixedThreadPool()
- Scheduled Thread Pool : 用来调度即将执行的任务的线程池,代码:Executors.newScheduledThreadPool()
- Single Thread Scheduled Pool : 只有一个线程,用来调度执行将来的任务,代码:Executors.newSingleThreadScheduledExecutor()
一旦你创建了一个线程池,你就可以往池中通过不同的方法提交执行任务,可提交 Runnable 或者 Callable 到线程池中,该方法返回一个 Future 实例表示任务的状态,如果你提交一个 Runnable ,那么如果任务完成后 Future 对象返回 null。
例如,你编写下面的 Callable:
private final class StringTask extends Callable<String>{ public String call(){ //Long operations return "Run"; } }
如果你想使用4个线程来执行这个任务10次,那么代码如下:
ExecutorService pool = Executors.newFixedThreadPool(4); for(int i = 0; i < 10; i++){ pool.submit(new StringTask()); }
但你必须手工的关闭线程池来结束所有池中的线程:
pool.shutdown();
如果你不这么做,JVM 并不会去关闭这些线程;另外你可以使用 shutdownNow() 的方法来强制关闭线程池,那么执行中的线程也会被中断,所有尚未被执行的任务也将不会再执行。
但这个例子中,你无法获取任务的执行状态,因此我们需要借助 Future 对象:
ExecutorService pool = Executors.newFixedThreadPool(4); List<Future<String>> futures = new ArrayList<Future<String>>(10); for(int i = 0; i < 10; i++){ futures.add(pool.submit(new StringTask())); } for(Future<String> future : futures){ String result = future.get(); //Compute the result } pool.shutdown();
不过这段代码稍微有点复杂,而且有不足的地方。如果第一个任务耗费非常长的时间来执行,然后其他的任务都早于它结束,那么当前线程就无法在第一个任务结束之前获得执行结果,但是别着急,Java 为你提供了解决方案——CompletionService。
一个 CompletionService 就是一个服务,用以简化等待任务的执行结果,实现的类是 ExecutorCompletionService,该类基于 ExecutorService,因此我们可试试下面的代码:
ExecutorService threadPool = Executors.newFixedThreadPool(4); CompletionService<String> pool = new ExecutorCompletionService<String>(threadPool); for(int i = 0; i < 10; i++){ pool.submit(new StringTask()); } for(int i = 0; i < 10; i++){ String result = pool.take().get(); //Compute the result } threadPool.shutdown();通过这段代码,我们可以根据执行结束的顺序获取对应的结果,而无需维护一个 Future 对象的集合。
这就是本文的全部,通过 Java 为我们提供的各种工具,可以方便的进行多任务的编程,通过使用 Executors、ExecutorService 以及 CompletionService 等工具类,我们可以创建复杂的并行任务执行算法,而且可以轻松改变线程数。
希望这篇短文能有助于你对并发编程的理解。
---
Executor 是 java5 下的一个多任务并发执行框架(Doug Lea),可以建立一个类似数据库连接池的线程池来执行任务。这个框架主要由三个接口和其相应的具体类组成。Executor、 ExecutorService 和 ScheduledExecutorService 。
1 、 Executor 接口:是用来执行 Runnable 任务的;它只定义一个方法- execute(Runnable command);执行 Ruannable 类型的任务。
2 、 ExecutorService 接口: 继承Executor接口,提供了执行Callable任务和中止任务执行的服务。
3 、 ScheduledExecutorService 接口:继承 ExecutorService 接口,提供了按排程执行任务的服务。
4 、 Executors 类:为了方便使用, 建议使用 Executors的工具类来得到 Executor 接口的具体对象。
Executors 类有几个重要的方法,在这里简明一下:
1 、 callable(Runnable task): 将 Runnable 的任务转化成 Callable 的任务
2 、 newSingleThreadExecutor(): 产生一个 ExecutorService 对象,这个对象只有一个线程可用来执行任务,若任务多于一个,任务将按先后顺序执行。
3 、 newCachedThreadPool(): 产生一个 ExecutorService 对象,这个对象带有一个线程池,线程池的大小会根据需要调整,线程执行完任务后返回线程池,供执行下一次任务使用。
4 、 newFixedThreadPool(int poolSize): 产生一个 ExecutorService 对象,这个对象带有一个大小为 poolSize 的线程池,若任务数量大于 poolSize ,任务会被放在一个 queue 里顺序执行。
5 、 newSingleThreadScheduledExecutor(): 产生一个 ScheduledExecutorService 对象,这个对象的线程池大小为 1 ,若任务多于一个,任务将按先后顺序执行。
6 、 newScheduledThreadPool(int poolSize): 产生一个 ScheduledExecutorService 对象,这个对象的线程池大小为 poolSize ,若任务数量大于 poolSize ,任务会在一个 queue 里等待执行 。
有关Executor框架其它类的说明请参看JAVA 5 的 API文档
下面是几个简单的例子,用以示例Executors中几个主要方法的使用。
1、 Task.java 任务
2、 SingleThreadExecutorTest.java 单线程执行程序的测试
3、 CachedThreadPoolTest.java 线程池线程执行程序的测试
4、 FixedThreadPoolTest.java 线程池线程执行程序的测试(线程数固定)
5、 DaemonThreadFactory.java 守护线程生成工厂
6、 MaxPriorityThreadFactory.java 大优先级线程生成工厂
7、 MinPriorityThreadFactory.java 小优先级线程生成工厂
8、 ThreadFactoryExecutorTest.java 在自定义线程生成工厂下的测试
1、 Task.java 任务
- package com.thread;
- //可执行任务
- public class Task implements Runnable {
- // 中断信号
- volatile boolean stop = false;
- // 该任务执行的次数
- private int runCount = 0;
- // 任务标识
- private int taskId;
- public Task(int taskId) {
- this.taskId = taskId;
- System.out.println("Create Task-" + taskId);
- }
- // 执行任务
- public void run() {
- while (!stop) {
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- System.out.println("Task interrupted...");
- }
- // 线程运行3次后,中断信号置为true
- if (++runCount == 3)
- stop = true;
- // 输出一些语句
- System.out.println("" + Thread.currentThread().toString()
- + "\t\t\t\t execute Task-" + taskId + "'s " + runCount
- + "th run. ");
- }
- }
- }
2、 SingleThreadExecutorTest.java 单线程执行程序的测试
- package com.thread;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class SingleThreadExecutorTest {
- public static void main(String[] args) {
- try {
- // 创建一个单线程执行程序
- ExecutorService executorService = Executors.newSingleThreadExecutor();
- for (int i =1; i <= 3; i++) {
- executorService.execute(new Task(i));
- }
- executorService.shutdown();
- } catch (Exception e) {}
- }
- }
3、 CachedThreadPoolTest.java 线程池线程执行程序的测试
- package com.thread;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class CachedThreadPoolTest {
- public static void main(String[] args) {
- try {
- // 建新线程的线程池,如果之前构造的线程可用则重用它们
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i =1; i <= 4; i++) {
- executorService.execute(new Task(i));
- }
- executorService.shutdown();
- } catch (Exception e) {}
- }
- }
4、 FixedThreadPoolTest.java 线程池线程执行程序的测试(线程数固定)
- package com.thread;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class FixedThreadPoolTest {
- public static void main(String[] args) {
- try {
- // 创建固定线程数的线程池,以共享的无界队列方式来运行这些线程
- ExecutorService executorService = Executors.newFixedThreadPool(2);
- for (int i =1; i <= 5; i++) {
- executorService.execute(new Task(i));
- }
- executorService.shutdown();
- } catch (Exception e) {}
- }
- }
5、 DaemonThreadFactory.java 守护线程生成工厂
- package com.thread;
- import java.util.concurrent.ThreadFactory;
- public class DaemonThreadFactory implements ThreadFactory {
- //创建一个守护线程
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setDaemon(true);
- return t;
- }
- }
---------------------
当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态,以后不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。 与它相似的还有一个shutdownNow(),它通过调用Thread.interrupt来实现线程的立即退出。
示例
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.ScheduledExecutorService;
- public class Ch09_Executor {
- private static void run(ExecutorService threadPool) {
- for(int i = 1; i < 5; i++) {
- final int taskID = i;
- threadPool.execute(new Runnable() {
- @Override
- public void run() {
- for(int i = 1; i < 5; i++) {
- try {
- Thread.sleep(20);// 为了测试出效果,让每次任务执行都需要一定时间
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("第" + taskID + "次任务的第" + i + "次执行");
- }
- }
- });
- }
- threadPool.shutdown();// 任务执行完毕,关闭线程池
- }
- public static void main(String[] args) {
- // 创建可以容纳3个线程的线程池
- ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
- // 线程池的大小会根据执行的任务数动态分配
- ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- // 创建单个线程的线程池,如果当前线程在执行任务时突然中断,则会创建一个新的线程替代它继续执行任务
- ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
- // 效果类似于Timer定时器
- ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
- run(fixedThreadPool);
- // run(cachedThreadPool);
- // run(singleThreadPool);
- // run(scheduledThreadPool);
- }
- }
CachedThreadPool
CachedThreadPool会创建一个缓存区,将初始化的线程缓存起来。会终止并且从缓存中移除已有60秒未被使用的线程。
如果线程有可用的,就使用之前创建好的线程,
如果线程没有可用的,就新创建线程。
- 重用:缓存型池子,先查看池中有没有以前建立的线程,如果有,就reuse;如果没有,就建一个新的线程加入池中
- 使用场景:缓存型池子通常用于执行一些生存期很短的异步型任务,因此在一些面向连接的daemon型SERVER中用得不多。
- 超时:能reuse的线程,必须是timeout IDLE内的池中线程,缺省timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。
- 结束:注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。
- // 线程池的大小会根据执行的任务数动态分配
- ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- public static ExecutorService newCachedThreadPool() {
- return new ThreadPoolExecutor(0, //core pool size
- Integer.MAX_VALUE, //maximum pool size
- 60L, //keep alive time
- TimeUnit.SECONDS,
- new SynchronousQueue<Runnable>());
- }
执行结果:
- 第1次任务的第1次执行
- 第4次任务的第1次执行
- 第3次任务的第1次执行
- 第2次任务的第1次执行
- 第3次任务的第2次执行
- 第4次任务的第2次执行
- 第2次任务的第2次执行
- 第1次任务的第2次执行
- 第2次任务的第3次执行
- 第4次任务的第3次执行
- 第3次任务的第3次执行
- 第1次任务的第3次执行
- 第2次任务的第4次执行
- 第1次任务的第4次执行
- 第3次任务的第4次执行
- 第4次任务的第4次执行
4个任务是交替执行的 。
FixedThreadPool
在FixedThreadPool中,有一个固定大小的池。
如果当前需要执行的任务超过池大小,那么多出的任务处于等待状态,直到有空闲下来的线程执行任务,
如果当前需要执行的任务小于池大小,空闲的线程也不会去销毁。
- 重用:fixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程
- 固定数目:其独特之处在于,任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子
- 超时:和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),
- 使用场景:所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器
- 源码分析:从方法的源代码看,cache池和fixed 池调用的是同一个底层池,只不过参数不同:
fixed池线程数固定,并且是0秒IDLE(无IDLE)
cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE
- // 创建可以容纳3个线程的线程池
- ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
- public static ExecutorService newFixedThreadPool(int nThreads) {
- return new ThreadPoolExecutor(nThreads, //core pool size
- nThreads, //maximum pool size
- 0L, //keep alive time
- TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>());
- }
执行结果:
- 第1次任务的第1次执行
- 第3次任务的第1次执行
- 第2次任务的第1次执行
- 第3次任务的第2次执行
- 第2次任务的第2次执行
- 第1次任务的第2次执行
- 第3次任务的第3次执行
- 第1次任务的第3次执行
- 第2次任务的第3次执行
- 第3次任务的第4次执行
- 第1次任务的第4次执行
- 第2次任务的第4次执行
- 第4次任务的第1次执行
- 第4次任务的第2次执行
- 第4次任务的第3次执行
- 第4次任务的第4次执行
创建了一个固定大小的线程池,容量为3,然后循环执行了4个任务。由输出结果可以看到, 前3个任务首先执行完,然后空闲下来的线程去执行第4个任务 。
SingleThreadExecutor
SingleThreadExecutor得到的是一个单个的线程,这个线程会保证你的任务执行完成。
如果当前线程意外终止,会创建一个新线程继续执行任务,这和我们直接创建线程不同,也和newFixedThreadPool(1)不同。
- // 创建单个线程的线程池,如果当前线程在执行任务时突然中断,则会创建一个新的线程替代它继续执行任务
- ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
- public static ExecutorService newSingleThreadExecutor() {
- return new FinalizableDelegatedExecutorService
- (new ThreadPoolExecutor(1, //core pool size
- 1, //maximum pool size
- 0L, //keep alive time
- TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>()));
- }
执行结果:
- 第1次任务的第1次执行
- 第1次任务的第2次执行
- 第1次任务的第3次执行
- 第1次任务的第4次执行
- 第2次任务的第1次执行
- 第2次任务的第2次执行
- 第2次任务的第3次执行
- 第2次任务的第4次执行
- 第3次任务的第1次执行
- 第3次任务的第2次执行
- 第3次任务的第3次执行
- 第3次任务的第4次执行
- 第4次任务的第1次执行
- 第4次任务的第2次执行
- 第4次任务的第3次执行
- 第4次任务的第4次执行
4个任务是顺序执行的 。
ScheduledThreadPool
ScheduledThreadPool是一个固定大小的线程池,与FixedThreadPool类似,执行的任务是定时执行。
- // 效果类似于Timer定时器
- ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
- public ScheduledThreadPoolExecutor(int corePoolSize) {
- super(corePoolSize, //core pool size
- Integer.MAX_VALUE, //maximum pool size
- 0, //keep alive time
- TimeUnit.NANOSECONDS,
- new DelayedWorkQueue());
- }
执行结果:
- 第1次任务的第1次执行
- 第2次任务的第1次执行
- 第3次任务的第1次执行
- 第2次任务的第2次执行
- 第1次任务的第2次执行
- 第3次任务的第2次执行
- 第2次任务的第3次执行
- 第1次任务的第3次执行
- 第3次任务的第3次执行
- 第2次任务的第4次执行
- 第1次任务的第4次执行
- 第3次任务的第4次执行
- 第4次任务的第1次执行
- 第4次任务的第2次执行
- 第4次任务的第3次执行
- 第4次任务的第4次执行
——与FixedThreadPool的区别?