今天遇到下载资金流水记录的场景。下载的数据源于分页查询,一次5000条。当数据量到达十万级的时候,仅仅通过for循环、每次设置pageNum,查询的等待时间超过了容忍的范围。下面示例展示了采用Callable和Future进行多线程查询并使用CountDownLatch进行多线程同步。
// 进行首次查询(略),获取总页数
int totalPage;
// 设置计数器,从0开始
final CountDownLatch countDownLatch = new CountDownLatch(totalPage - 1);
// 定义Future数组,数组大小和计数器个数相同
Future<PageResult<FundRecordDTO>>[] futures = new Future[totalPage - 1];
for (int i = 0; i < totalPage; i++) {
// 重设分页参数,从1开始
final PageArg pageArg = new PageArg();
pageArg.setPageSize(ExcelHelper.pageSize);
pageArg.setPageIndex(i + 1);
// 将各个PageResult塞到线程池的各个线程里,返回Future数组
futures[i] = threadPool.submit(new Callable<PageResult<FundRecordDTO>>() {
// 返回取得的PageResult
@Override
public PageResult<FundRecordDTO> call() throws Exception {
PageResult<FundRecordDTO> pageResult = new PageResult<FundRecordDTO>();
try {
pageResult = fundRecordService.findFundRecordPage(pageArg, fundRecordParam);
} catch (Exception e) {
throw e;
} finally {
// 线程完成任务后通过countDownLatch.countDown()来通知CountDownLatch对象,计数器-1
countDownLatch.countDown();
}
return pageResult;
}
});
}
// 所有任务执行完毕后触发事件,唤醒await在latch上的主线程
countDownLatch.await();
// 合并记录
for (int j = 0; j < totalPage; j++) {
if (futures[j] != null && futures[j].get().getData() != null) {
fileDataList.addAll(futures[j].get().getData());
}
}
上面的例子用到了Callable、Future和CountDownLatch三个常用的多线程工具类,下面我们分别来了解下。
Callable和Future
Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其他线程执行的任务。
Callable和Runnable的区别如下:
- Callable定义的方法是call,而Runnable定义的方法是run。
- Callable的call方法可以有返回值,而Runnable的run方法不能有返回值。
- Callable的call方法可抛出异常,而Runnable的run方法不能抛出异常。
public
interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果、设置结果操作。
public interface Future<V> {
// 可以取消任务的执行,参数为true表示立即中断任务的执行,参数为false表示允许正在运行的任务运行完成。
boolean cancel(boolean mayInterruptIfRunning);
// 查询是否取消掉
boolean isCancelled();
// 查询是否完成
boolean isDone();
// 等待计算完成,获取计算结果。
V get() throws InterruptedException, ExecutionException;
// 在超时时间内等待计算完成,获取计算结果。
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
CountDownLatch
CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。
和其它并发工具类,如CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。这个初始值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。
与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。