这篇文章通过介绍线程池的常见用法,总结前面学习到的内容。
主要包括
- ThreadPoolExecutor的使用
- ScheduledThreadPoolExecutor的使用
- ExecutorCompletionService的使用
1. 统计某个区间内数字和
这是一个常见的问题,比如计算1到1000000之间的数字和,通常情况下,一个线程去遍历计算即可,既然学到了多线程,我们可以将该任务切割,分成多个子任务并交给线程池来解决,子任务解决后将这些任务的结果归并,就可以得到我们要的值了。虽然这个问题很简单,但是它的解决思路可以用于解决其他类似的问题。
先看下面代码:
public class AsyncEngine {
/**
* 线程池的线程数量不需要太多,它是和CPU核数量有关,太多反而上下文切换效率不好
*/
private static ExecutorService executorService =
new ThreadPoolExecutor(2, 10, 30, TimeUnit.SECONDS, new SynchronousQueue());
/**
* 定时任务线程池
*/
private static ScheduledExecutorService scheduledExecutorService
= new ScheduledThreadPoolExecutor(2);
/**
* 完成服务线程池
*/
private static CompletionService completionService
= new ExecutorCompletionService(executorService);
/**
* 异步执行任务,所有任务执行完成后该方法才返回,如果任务执行失败,该任务返回值为null
* @param tasks 待执行的任务
* @param <T> 任务的返回值
* @return
*/
public static <T> List<T> concurrentExecute(List<Callable<T>> tasks) {
if (tasks == null || tasks.size() == 0) {
return Lists.newArrayList();
}
List<T> res = Lists.newArrayList();
try {
List<Future<T>> futures = executorService.invokeAll(tasks);
for (Future<T> future : futures) {
T t = null;
try {
t = future.get();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (Throwable e) {
e.printStackTrace();
}
res.add(t);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return res;
}
/**
* 定时执行任务
* @param runnable 执行的任务
* @param initialDelay 初始延时
* @param delay 任务间的等待时间
* @param unit 时间单位
* @return
*/
public static ScheduledFuture<?> scheduledWithFixedDelay(Runnable runnable,
long initialDelay,
long delay, TimeUnit unit) {
return scheduledExecutorService.scheduleWithFixedDelay(runnable,
initialDelay,
delay, unit);
}
/**
* 完成服务
* @param tasks
* @param <T>
* @return
*/
public static <T> CompletionService completionExecute(List<Callable<T>> tasks) {
if (tasks == null || tasks.size() == 0) {
return completionService;
}
for (Callable<T> task : tasks) {
completionService.submit(task);
}
return completionService;
}
}
这段代码是在项目中实际使用的代码,只是作了部分改动,它是一个异步执行引擎,方法concurrentExecute并发执行任务,当这些任务执行完成后,该方法才会返回。我们定义了一个线程池,指定核心线程数为2,最大线程数为10,线程数定义太大无意义,因为它是和CPU核数是有关系的。
concurrentExecute方法中,调用线程池的invokeAll方法,该方法返回Future的列表,然后遍历该列表,Future的get方法会阻塞,直到该任务执行完成,将任务返回结果放入List中。注意,当任务抛出异常时,返回结果为null,该null值也一并放到List中作为concurrentExecute方法的返回值。
有了这个异步执行引擎,我们继续看下怎么实现统计某个区间内的数字和,看下代码:
public class Sum {
/**
* 多线程并发计算
* @param min
* @param max
* @return
*/
public static long sum1(int min, int max) {
if (min > max) {
return 0L;
}
List<Callable<Long>> tasks = Lists.newArrayList();
while (min < max) {
final int left = min;
final int right = max;
//分割任务,每个任务最多只相加1000个数
Callable<Long> task = new Callable<Long>() {
@Override
public Long call() throws Exception {
long sum = 0;
int r = (left + 1000) < right ? (left + 1000) : right;
for (int i = left; i < r; i++) {
sum += i;
}
return sum;
}
};
tasks.add(task);
min += 1000;
}
//异步执行,执行完成后该方法返回
List<Long> res = AsyncEngine.concurrentExecute(tasks);
long sum = 0;
//归并结果
for (Long count : res) {
sum += count;
}
return sum;
}
/**
* 单线程计算
* @param min
* @param max
* @return
*/
public static long sum2(int min, int max) {
long sum = 0;
for (int i = min; i < max; i++) {
sum += i;
}
return sum;
}
public static void main(String[] args) {
System.out.println(sum1(4, 9999));
System.out.println(sum2(4, 9999));
}
}
并发统计和的重点在于分割任务,每个任务最多相加1000个数字。子任务分割完成后,一并交给异步执行引擎处理,返回一个列表,该列表保存了每个任务的返回值,最后归并这个列表即可。这段代码也给出了单线程的代码,便于对比。
2. 定时任务
这里的定时任务很简单,5秒过后开始执行任务,任务执行完成等待1秒再次执行该任务,模拟闹钟的秒针运行,每秒嘀嗒一次。上面讲到的异步引擎AsyncEngine已经写好了定时相关的代码,直接运行目标任务即可。
public class Alarm {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("da");
}
};
//定时调度,模拟时钟的秒针运行
AsyncEngine.scheduledWithFixedDelay(runnable, 5, 1, TimeUnit.SECONDS);
}
}
3. 完成服务
利用CompletionService来实现生产者消费者模式,对于第一个例子中讲到的统计区间数字和,主线程其实就是一个消费者,线程池中的线程是生产者,这些生产者计算各个任务,消费者等待这些任务执行结束。
这里面的问题是,通过concurrentExecute方法执行任务,需要等到所有任务执行结束该方法才能返回,如果某些任务已经完成,消费者能够即时的消费,显然更加合理。因此我们在AsyncEngine里添加了completionExecute方法,这个方法提交所有的任务到线程池,返回CompletionService。消费者(主线程)通过CompletionService的take方法取任务Future,取到的Future肯定是已经结束的任务,当take方法阻塞,说明还没有已经结束的任务。看下Sum类的实现:
public class Sum {
/**
* 多线程并发计算
* @param min
* @param max
* @return
*/
public static long sum1(int min, int max) {
if (min > max) {
return 0L;
}
List<Callable<Long>> tasks = Lists.newArrayList();
while (min < max) {
final int left = min;
final int right = max;
//分割任务,每个任务最多只相加1000个数
Callable<Long> task = new Callable<Long>() {
@Override
public Long call() throws Exception {
long sum = 0;
int r = (left + 1000) < right ? (left + 1000) : right;
for (int i = left; i < r; i++) {
sum += i;
}
return sum;
}
};
tasks.add(task);
min += 1000;
}
//使用CompletionService执行任务
CompletionService<Long> completionService = AsyncEngine.completionExecute(tasks);
long sum = 0;
for ( int i = 0; i < tasks.size(); i++) {
try {
sum += completionService.take().get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
return sum;
}
}
这个类和前面那个Sum类非常类似,这里只是将任务提交给了CompletionService执行,主线程通过CompletionService的take方法取任务的Future,将任务的返回值累加并返回。