最近在项目里用到了多线程,包括线程池的创建,多个线程同步等,所以对executor框架简单复习一下。因为是简单复习,所以不会介绍太多概念,只是对一些基础知识点列举,并给出几个实际问题及其解决方法。
一、executor框架在java5引入,为并发编程提供了一堆新的启动、调度和管理线程的API。它在java.util.cocurrent包下,其内部使用了线程池机制,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,更易管理,效率更好(用线程池实现,节约开销),它的主要内容包括:threadPool,Executor,Executors,ExecutorService,CompletionService,Future,Callable,以及CountDownLatch 等工具类。下面是一些基础概念:
1. Executor接口定义了一个execute(Runnable command)方法,接收一个Runnable实例。
2. ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。
3. Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。 其中两种是:
public static ExecutorService newFixedThreadPool(int nThreads)创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线 程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
4. Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,两者的区别如下:
a. Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable<T> task) 方法来执行,并且返回一个 <T>Future<T>,是表示任务等待完成的 Future。
b.Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 RunnableCallable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。
c.当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。
二。实际问题
问题1. 我们需要某件事准备好之后,开始执行一组任务。而且要这组任务都结束后,才进行后续动作。
CountDownLatch,它是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 用给定的计数初始化 CountDownLatch。每个被等待的工作线程完成后,调用了 countDown() 方法,计数器减1。在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回,不在阻塞。代码如下:
public String test1() {
final int N = 3;
CountDownLatch doneSignal = new CountDownLatch(N);
CountDownLatch startSignal = new CountDownLatch(1);//开始执行信号
for (int i = 1; i <= N; i++) {
new Thread(new Worker(i, doneSignal, startSignal)).start();//线程启动了
}
System.out.println("begin------------");
startSignal.countDown();//开始执行啦
try {
doneSignal.await();//这句使得主线程等待所有的线程执行完毕,才会继续往下走,输出OK
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Ok");
return "done";
}
在test1里,当startSignal信号变为0时,for循环里的N个工作线程才开始执行。并且等这N个线程都执行结束后,主线程才能输出OK;worker代码如下:
class Worker implements Runnable {
private final CountDownLatch doneSignal;
private final CountDownLatch startSignal;
private int beginIndex;
Worker(int beginIndex, CountDownLatch doneSignal,
CountDownLatch startSignal) {
this.startSignal = startSignal;
this.beginIndex = beginIndex;
this.doneSignal = doneSignal;
}
public void run() {
try {
startSignal.await(); //等待开始执行信号的发布
beginIndex = (beginIndex - 1) * 2+ 1;
for (int i = beginIndex; i <= beginIndex + 2; i++) {
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
doneSignal.countDown();//调用countDown表示自己执行结束。共享计数减1
}
}
}
问题2. 文件批量下载,每个文件通过一个线程下载,需要将所有文件都下载后,打包成zip压缩文件返回(文件上传下载会在后面介绍)。
ExecutorService的submit(Callable<T> task) 方法执行一个Callbale实例,并且返回一个 <T>Future<T>,是表示任务等待完成的 Future。通过跟踪future,可以判断任务是否完成。关键代码如下:
List<File>fileAll=Lists.new ArrayList();
...初始化fileAll
List<Future<String>> futures=Lists.newArrayList();//跟踪每个任务的执行结果
for(int i=0;i<fileAll.size();i++){
File e=fileAll.get( i );
String filename=e.getFilename();
String url = MessageFormat.format( downloadUrl, e.getUrl() );
FileDownloadTask fs=new FileDownloadTask( file,filename,url );//下载文件的任务
futures.add( scheduledExecutorComponent.submit(fs));//将任务返回加入列表。进行跟踪
}
for (Future<String> fs : futures){//此循环跟踪每个任务的执行结果
try{
while(!fs.isDone());//Future返回如果没有完成,则一直循环等待,直到Future返回完成
LOG.debug( "文件下载结果:"+documentid+":"+fs.get() );
}catch(Exception e){
e.printStackTrace();
}
}
问题3 同问题1以及问题2的场景类似。只不过我们给出另外一种解决方法:invokeAll.关键代码如下:
public String test2() {
List<Callable<Integer>> tasks = new ArrayList<Callable<Integer>>();
Callable<Integer> task = null;
for (int i = 0; i < 5; i++)
{
task = new Callable<Integer>()
{
@Override
public Integer call() throws Exception
{
int ran = new Random().nextInt(1000);
Thread.sleep(ran);
System.out.println(Thread.currentThread().getName()+" 执行了 " + ran );
return ran;
}
};
tasks.add(task);
}
long s = System.currentTimeMillis();
List<Future<Integer>> results = null;
try {
results = this.scheduledExecutorComponent.invokeAll(tasks);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行任务消耗了 :" + (System.currentTimeMillis() - s) +"毫秒");
for (int i = 0; i < results.size(); i++)
{
try
{
System.out.println(results.get(i).get());//3
} catch (Exception e)
{
e.printStackTrace();
}
}
return "ok";
}
如果有一个任务执行失败,3初会报异常,所以,invokeAll 还可以结合ExecutorCompletionService来使用,通过一个blockingQueue来管理,一旦有线程执行失败,可以立即获得结果。具体请参考:https://stackoverflow.com/questions/18202388/how-to-use-invokeall-to-let-all-thread-pool-do-their-taski
invokeAll是阻塞方法,它必须等待所有的任务执行完成后统一返回,一方面内存持有的时间长;另一方面响应性也有一定的影响、所以对于问题场景,我们更倾向于使用前面两种方法。