并发编程与任务建模
一、首先说Thread,Runnable与Callable
编写多线程程序是为了实现多任务的并发执行,从而能够更好地与用户交互。一般有三种方法,Thread,Runnable,Callable.
Runnable和Callable的区别是:
(1)Callable规定的方法是call(),Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以
(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
1、通过实现Runnable接口来创建Thread线程:
步骤1:创建实现Runnable接口的类:
class SomeRunnable implements Runnable { public void run(){ //dosomething here }
}
步骤2:创建一个类对象:
Runnable oneRunnable = newSomeRunnable();
步骤3:由Runnable创建一个Thread对象:
Thread oneThread = newThread(oneRunnable);
步骤4:启动线程:
oneThread.start();
至此,一个线程就创建完成了。
注释:线程的执行流程很简单,当执行代码oneThread.start();时,就会执行oneRunnable对象中的voidrun();方法,
该方法执行完成后,线程就消亡了。
2、与方法1类似,通过实现Callable接口来创建Thread线程:其中,Callable接口(也只有一个方法)定义如下:
public interfaceCallable<V>{ V call() throws Exception; }
步骤1:创建实现Callable接口的类SomeCallable<Integer>(略);
步骤2:创建一个类对象:
Callable<Integer> oneCallable = newSomeCallable<Integer>();
步骤3:由Callable<Integer>创建一个FutureTask<Integer>对象:
FutureTask<Integer> oneTask = newFutureTask<Integer>(oneCallable);
注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。
步骤4:由FutureTask<Integer>创建一个Thread对象:
Thread oneThread = new Thread(oneTask);
步骤5:启动线程:
oneThread.start();
至此,一个线程就创建完成了。
3、通过继承Thread类来创建一个线程:
步骤1:定义一个继承Thread类的子类:
class SomeTheadextends Thraad{ public void run(){ //do something here } }
步骤2:构造子类的一个对象:
SomeThread oneThread = new SomeThread();
步骤3:启动线程:
oneThread.start();
至此,一个线程就创建完成了。
注释:这种创建线程的方法不够好,主要是因为其涉及运行机制问题,影响程序性能。
4、通过线程池来创建线程:
步骤1:创建线程池:
ExecutorService pool =Executors.newCachedThreadPool();
步骤2:通过Runnable对象或Callable对象将任务提交给ExecutorService对象:
Future<Integer>submit(Callable<Integer> task);
注释:Future是一个接口,它的定义如下:
public interfaceFuture<T>{ V get() throws ...; V get(long timeout, TimeUnit unit) throws...; void cancle(boolean mayInterrupt); boolean isCancelled(); boolean isDone(); }
至此,一个线程就创建完成了。
注释:线程池需调用shutdown();方法来关闭线程。
5、通过事件分配线程直接使用程序中的原有线程:
使用方法:
直接调用EventQueue类的静态方法invokeLater():
EventQueue.invokeLater(oneRunnable);
注释:调用EventQueue.invokeLater(oneRunnable);会直接执行oneRunnable对象中的run()方法。
二、再说Future与FutureTask
Future是一个接口,FutureTask是其常用实现类。表示异步任务,是还没有完成的任务给出的未来结果。它们位于java.util.concurrent包下,可以对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
Future接口定义如下:
public interfaceFuture<V> { boolean cancel(booleanmayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException,ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException,ExecutionException, TimeoutException; }
其中的5个方法作用如下:
Cancel()方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。
isCancelled()方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
isDone()方法表示任务是否已经完成,若任务完成,则返回true;
get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
get(longtimeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
也就是说Future提供了三种功能:
1)判断任务是否完成;
2)能够中断任务;
3)能够获取任务执行结果。
因为Future只是一个接口,所以是无法直接用来创建对象使用的,详细看看FutureTask。
我们先来看一下FutureTask的实现:
public classFutureTask<V> implements RunnableFuture<V>
FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:
public interfaceRunnableFuture<V> extends Runnable, Future<V> { void run(); }
可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
FutureTask提供了2个构造器:
publicFutureTask(Callable<V> callable) {}
publicFutureTask(Runnable runnable, V result) {}
事实上,FutureTask是Future接口的一个唯一实现类。是为了弥补Thread的不足而设计的,它可以让程序员准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果(如果有需要)。
三、接着说Executor、Executors、ExecutorService、ThreadPoolExecutor、ScheduledThreadPoolExecutor以及CompletionService与ExecutorCompletionService。
这些都表示任务执行器或与异步任务相关的服务,关于它们的详细信息请参看:http://man.ddvip.com/program/java_api_zh/index.html?java/util/concurrent/Executor.html。
其中,Executor接口用于执行已提交的Runnable 任务的对象。此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。通常使用 Executor 而不是显式地创建线程。ExecutorService实现了Executor接口,这是一个使用更广泛的接口。ThreadPoolExecutor 类提供一个可扩展的线程池实现。Executors类为这些 Executor 提供了便捷的工厂方法。
其中ScheduledThreadPoolExecutor(STPE)继承自ThreadPoolExecutor,实现了Executor,ExecutorService, ScheduledExecutorService接口,它是线程池类中的重中之重,功能多样,广受欢迎。之后将详细介绍一下这个神奇的类。
CompletionService接口用于将生产新的异步任务与使用已完成任务的结果分离开来的服务。生产者 submit 执行的任务。使用者 take 已完成的任务,并按照完成这些任务的顺序处理它们的结果。例如,CompletionService 可以用来管理异步 IO ,执行读操作的任务作为程序或系统的一部分提交,然后,当完成读操作时,会在程序的不同部分执行其他操作,执行操作的顺序可能与所请求的顺序不同。
通常,CompletionService 依赖于一个单独的Executor 来实际执行任务,在这种情况下,CompletionService 只管理一个内部完成队列。ExecutorCompletionService 类提供了此方法的一个实现。
u 详解ScheduledThreadPoolExecutor:
自jdk1.5开始,Java开始提供ScheduledThreadPoolExecutor类来支持周期性任务的调度,在这之前,这些工作需要依靠Timer/TimerTask或者其它第三方工具来完成。但Timer有着不少缺陷,如Timer是单线程模式,调度多个周期性任务时,如果某个任务耗时较久就会影响其它任务的调度;如果某个任务出现异常而没有被catch则可能导致唯一的线程死掉而所有任务都不会再被调度。ScheduledThreadPoolExecutor解决了很多Timer存在的缺陷。
先来看看ScheduledThreadPoolExecutor的实现模型,它通过继承ThreadPoolExecutor来重用线程池的功能,里面做了几件事情:
为线程池设置了一个DelayedWorkQueue,该queue同时具有PriorityQueue(优先级大的元素会放到队首)和DelayQueue(如果队列里第一个元素的getDelay返回值大于0,则take调用会阻塞)的功能
将传入的任务封装成ScheduledFutureTask,这个类有两个特点,实现了java.lang.Comparable和java.util.concurrent.Delayed接口,也就是说里面有两个重要的方法:compareTo和getDelay。ScheduledFutureTask里面存储了该任务距离下次调度还需要的时间(使用的是基于System#nanoTime实现的相对时间,不会因为系统时间改变而改变,如距离下次执行还有10秒,不会因为将系统时间调前6秒而变成4秒后执行)。getDelay方法就是返回当前时间(运行getDelay的这个时刻)距离下次调用之间的时间差;compareTo用于比较两个任务的优先关系,距离下次调度间隔较短的优先级高。那么,当有任务丢进上面说到的DelayedWorkQueue时,因为它有DelayQueue(DelayQueue的内部使用PriorityQueue来实现的)的功能,所以新的任务会与队列中已经存在的任务进行排序,距离下次调度间隔短的任务排在前面,也就是说这个队列并不是先进先出的;另外,在调用DelayedWorkQueue的take方法的时候,如果没有元素,会阻塞,如果有元素而第一个元素的getDelay返回值大于0(前面说过已经排好序了,第一个元素的getDelay不会大于后面元素的getDelay返回值),也会一直阻塞。
ScheduledFutureTask提供了一个run的实现,线程池执行的就是这个run方法。看看run的源码(本文的代码取自hotspot1.5.0_22,jdk后续版本的代码可能已经不一样了,如jdk1.7中使用了自己实现的DelayedWorkQueue,而不再使用PriorityQueue作为存储,不过从外面看它们的行为还是一样的,所以并不影响对ScheduledThreadPoolExecutor调度机制的理解):
public void run() { if (isPeriodic()){ runPeriodic(); }else{ ScheduledFutureTask.super.run(); } }
如果不是周期性任务就直接执行任务(也就是else部分),这个主要是用于实现ScheduledThreadPoolExecutor#schedule(Callablecallable, long delay, TimeUnit unit)和ScheduledThreadPoolExecutor#schedule(Runnablecommand, long delay, TimeUnit unit),后面会讲到它们的实现,这里先关注周期任务的执行方式。周期性任务执行的是runPeriodic(),看下它的实现:
private voidrunPeriodic() { boolean ok =ScheduledFutureTask.super.runAndReset(); boolean down = isShutdown(); // Reschedule if not cancelled and notshutdown or policy allows if(ok && (!down || (getContinueExistingPeriodicTasksAfterShutdownPolicy()&& !isTerminating()))) { long p = period; if (p > 0){ time += p; }else{ time = triggerTime(-p); } ScheduledThreadPoolExecutor.super.getQueue().add(this); }// This might have been the final executeddelayed // task. Wake up threads to check. else if (down){ interruptIdleWorkers(); } }
这里可以看到,先执行了任务本身(ScheduledFutureTask.super.runAndReset),这个调用有一个返回值,来看下它的实现:
protectedboolean runAndReset() { return sync.innerRunAndReset(); }
跟进去看下innerRunAndReset():
boolean innerRunAndReset() { if (!compareAndSetState(0, RUNNING)){ return false; } try { runner = Thread.currentThread(); callable.call(); // don't set result runner = null; return compareAndSetState(RUNNING, 0); } catch(Throwable ex) { innerSetException(ex); return false; } }
可以发现,这里需要关注的是第三个return,也就是如果任务执行出现了异常,会被catch且返回false.
继续看runPeriodic()方法,if里面,如果刚才任务执行的返回值是true且线程池还在运行就在if块中的操作,如果线程池被关闭了就做else if里的操作。也就是说,如果之前的任务执行出现的异常返回了false,那么if里以及else if里的代码都不会执行了,那有什么影响?接下来看看if里做了什么。
if里的代码很简单,分为两部分,一是计算这个任务下次调度的间隔,二是将任务重新放回队列中。回到出现异常的情况,如果刚才的任务执行出现了异常,就不会将任务再放回队列中,换而言之,也就是这个任务再也得不到调度了!但是,这并不影响其它周期任务的调度。
综上,可以看到,ScheduledThreadPoolExecutor执行周期性任务的模型就是:调度一次任务,计算并设置该任务下次间隔,将任务放回队列中供线程池执行。这里的队列起了很大的作用,且有一些特点:距离下次调度间隔短的任务总是在队首,队首的任务若距离下次调度的间隔时间大于0就无法从该队列的take()方法中拿到任务。
接下来看看ScheduledThreadPoolExecutor#schedule(Callablecallable, long delay, TimeUnit unit)和ScheduledThreadPoolExecutor#schedule(Runnablecommand, long delay, TimeUnit unit)这两个非周期性任务的实现方式,先看看它们的源码:
publicScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit) { if (command == null || unit == null){ throw new NullPointerException(); } ScheduledFutureTask<?> t = newScheduledFutureTask<Boolean>(command, null, triggerTime(delay, unit)); delayedExecute(t); return t; } public <V>ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit) { if (callable == null || unit == null){ throw new NullPointerException(); } ScheduledFutureTask<V> t = newScheduledFutureTask<V>(callable, triggerTime(delay, unit)); delayedExecute(t); return t; } private voiddelayedExecute(Runnable command) { if (isShutdown()) { reject(command); return; }// Prestart a thread if necessary. Wecannot prestart it // running the task because the task(probably) shouldn't be // run yet, so thread will just idle untildelay elapses. if (getPoolSize() < getCorePoolSize()){ prestartCoreThread(); } super.getQueue().add(command); }
实现方式也很简单,在创建ScheduledThreadPoolExecutor内部任务(即ScheduledFutureTask)的时候就将调度间隔计算并设置好,如果当前线程数小于设置的核心线程数,就启动一个线程(可能是线程池刚启动里面还没有线程,也可能是里面的线程执行任务时挂掉了。如果线程池中的线程挂掉了而又没有调用这些schedule方法谁去补充挂掉的线程?不用担心,线程池自己会处理的)去监听队列里的任务,然后将任务放到队列里,在任务执行间隔不大于0的时候,线程就可以拿到这个任务并执行。
周期性任务的入口(ScheduledThreadPoolExecutor#scheduleAtFixedRate(Runnablecommand, long initialDelay, long period, TimeUnit unit)和ScheduledThreadPoolExecutor#scheduleWithFixedDelay(Runnablecommand, long initialDelay, long delay, TimeUnit unit))与非周期性任务是类似的,它们处理方式不同的地方在于前文说到的ScheduledFutureTask#run()中。