android编程中,并发编程是不可或缺的一部分,几乎所有应用程序的开发中都会用到并发编程,今天我们就来聊聊并发编程的一些事儿,我们先从java对并发编程的基本支持说起。
转载请注明出处: http://blog.youkuaiyun.com/qinjunni2014/article/details/45055251
Runnable与Thread
Runnable与Thread相信大家一定都能熟练使用,我们今天在这里就不赘述了,但是仅仅靠使用Runnnable和Thread是不能满足我们并发编程全部需求的,比如我们需要能有效的控制当有大量并发任务时,任务执行的吞吐量,任务的取消等等。Java中还有一些与并发编程密切相关的类,使用它们能更好地帮我们控制并发编程中任务的合理执行,我先用一张图来大致描述一下有哪些类和接口,这些类和接口之间又怎么样的关系。
.
处于UML图最顶层的是一个接口Executor,这个接口有一个方法叫做execute,Executor的这个方法可以将任务的提交和任务如何执行(包括线程的使用细节,调度等)解耦,相比直接创建线程执行任务,java更建议我们使用Executor,例如使用
new Thread(new(RunnableTask())).start()
java更建议我们使用
Executor executor = <em>anExecutor</em>;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
来调度任务的执行。
Executor
Executor本身并不保证我们的任务一定会在不同的线程上执行,在最简单的实现中,我们的任务可以立即在调用者的线程上执行:
class DirectExecutor implements Executor {
public void execute(Runnable r) {
r.run();
}
}}
这种情况比较少用,更典型的情况是,我们会为每个任务新创建一个线程去执行,
class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
}
}}
ThreadPerTaskExecutor只是简单地为我们的任务分配一个线程去执行,如果我们同时需要执行1000个以上的任务,这个executor会同时开启1000+个线程去执行任务,这会给我们的机器造成比较大的开销,而且每个任务执行的效率也会很低下。因此我们需要在自己的Executor中,增加线程调度的逻辑,其中最简单地调度逻辑就是,每次开启一个线程执行一个任务,等待任务执行完毕,执行下一个任务。
class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
executor.execute(mActive);
}
}
}
每次execute一个runnable时,我们会将runnable重新包装,放入mTasks这个队列中,如何包装?就是在runnable结束之后调用scheduleNext从mTasks重取出一个新的任务并立即执行。AsyncTask中默认的执行方式就是这种。
Executor给我们规定的接口实在太少,接口ExecutorService则继承了Executor接口。
ExecutorService
ExecutorService接口在Executor的基础上,提供了更多的接口,用来管理任务的终止,而且提供了函数用来构造一个Future对象来追踪任务的执行程度。
ExecutorService内的接口函数主要有三种
跟shutdown相关的
- shutDown() 这个函数试图shutdown当前executor,拒绝新任务的提交,但是会等待之前提交的任务执行完毕
- shutDownNow(), 类似shutDown,区别在于它会终止当前正在执行的任务和正在等待执行的任务
- isShutDown() 判断是否已经shutdown,
- isTerminated() 判断是否shutdown之后,所有任务已经执行完毕,必须先调用shutdown…
invoke和await
- invokeAll 执行给定任务集,当所有任务完成之后,返回一个包括任务状态和结果的Future集合
- T invokeAny执行给定任务集,如果某个任务执行完毕,返回执行结果
- boolean awaitTermination(long timeout, TimeUnit unit) 这个函数会阻塞,直到在shutdown请求后所有任务执行完毕,或者超时 或者线程异常终止
submit系列
- Future submit(Callable task); 提交一个callable任务,并返回一个Future对象,从Future对象可以阻塞地获得执行结果。如果想在执行任务的时候立马阻塞等待结果,可以这样做
result = exec.submit(aCallable).get();
- Future submit(Runnable task, T result); 和上一个函数类似,区别在于前者是执行Callable对象,callable对象在执行时可以返回result,而这个函数是执行Runnable对象,返回结果是放在submit参数中的result
至此,有必要解释一下Callable和Runnable的区别。两则区别其实不大,它们的实例一般都运行在其他线程中,区别在于runnable没有返回值,而callable有,而且callable会抛出异常。
Callable的基本用法如下:
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(3000);
return 8;
}
};
Future<Integer> future = mExecutor.submit(callable);
try {
int result = future.get();
Log.e("Junli","result is: "+ result);
} catch (Exception e) {
e.printStackTrace();
}
Future
介绍了Callable,有必要再继续讲一下Future,Future代表了异步运算的结果,有了Future,我们就可以对异步任务进行简单地控制,比如取消,查询,获取结果等。Future类系的uml图如下所示:
Future接口的方法很明了,给出了查询,取消和 获取结果的几种方法,值得注意的是:
- isDone方法在正常结束、发生异常和取消时都会返回true
- cancel方法在任务已经运行完毕时,会失败,如果任务已经开始运行了,则参数mayInterruptIfRunning指定了任务是否可以被终止。
- get方法在任务没有结束前会阻塞,带时间参数的版本指定了最长等待时间
Future的典型用法一般都在ExecutorServices.submit时,submit会返回一个future对象,对任务进行基本的控制。如何构建一个Future对象呢?我们先回过头来继续讲之前的ExecutorService接口。
AbstractExecutorService
ExecutorService接口定义了一些方法,开发者如果需要自己去实现,岂不是坑爹,幸好java中已经帮我们实现了一些比较典型的ExecutorService,比如常用的ThreadPoolExecutor,它会开启一个线程池,每次允许固定数量的线程运行去执行任务。ThreadPoolExecutor实际上是继承自抽象类AbstractExecutorService。为什么要有这个抽象类,因为java认为有些方法的实现不同的Executor都是相同的,可以重用。我们来看看这个抽象的父类,实现了ExecutorService接口中的那些方法呢?
其实主要有两类:
第一类是submit方法系列
AbstractExecutorService中的submit方法会根据我们传入的任务生成一个RunnableFuture对象,然后调用子类的execute方法去执行这个RunnableFuture,最后返回这个RunnableFuture, 没错从前面的uml图中我们可以看到这个RunnableFuture就是同时继承自Runnable和Future,这样,这个接口的实现类就可以同时实现任务的运行和任务的控制。
invoke系列
AbstractExecutorService已经实现了一系列的invoke函数,比如invokeAll
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
if (tasks == null)
throw new NullPointerException();
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
boolean done = false;
try {
for (Callable<T> t : tasks) {
RunnableFuture<T> f = newTaskFor(t);
futures.add(f);
execute(f);
}
for (int i = 0, size = futures.size(); i < size; i++) {
Future<T> f = futures.get(i);
if (!f.isDone()) {
try {
f.get();
} catch (CancellationException ignore) {
} catch (ExecutionException ignore) {
}
}
}
done = true;
return futures;
} finally {
if (!done)
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}
ThreadPoolExecutor
有了这些实现,子类就只需要专心实现execute和shutdown的逻辑了。而ThreadPoolExecutor正是在这两个函数上下足了功夫。关于ThreadPollExecutor执行任务的具体方式,大家可以参考我前面写的两篇关于AsyncTask的文章。
这里再将要介绍下:
- 先安排core_pool_size数量的任务以core thread执行
- 然后将多余任务入工作队列
- 如果队列也满,则将多余任务以非core thread运行直到,
- 运行的线程数量达到maximum_pool_size,则调用rejectHandler拒绝任务
ThreadPoolExecutor的构造函数提供了五个参数来控制内部的具体细节,他们是
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
Executors工厂类
如果,用户每次在构造ThradPoolExecutor时都需要指定这五个参数,略显麻烦,java中有一个工厂类Executors,我们只需要调用其中的工厂方法就可以轻松构造出我们需要的ThreadPoolExecutor,比较常用的有:
- newFixedThreadPool 用这个方法构建出的ThreadPoolExecutor,coresize与maxmumPoolSize相同,这样就将线程池的大小固定。
- newCachedThreadPool 根据需要创建线程,60秒未使用的线程将会结束并从cache中移除,在大量任务时会重用之前使用过现在空闲的线程
- newScheduledThreadPool 会构建一个ScheduledExecutorService,这个类我们还未讲到,它是ThreadPoolExecutor的子类,顾名思义,它能够一定的delay或 周期性的去执行任务。我们先简要介绍下这个类的使用方法:
ScheduledExecutorService
这个类在ThreadPoolExecutor的基础上提供了一系列以schedule打头的方法:
- schedule
- scheduleAtFixedRate
- scheduleWithFixedDelay
- setContinueExistingPeriodicTasksAfterShutdownPolicy 这个函数控制在executor执行了shutdown之后是否继续执行哪些本应周期性执行的任务
ScheduledExecutorService mScheduledExecutor = Executors.newScheduledThreadPool(5);
Runnable runnable = new Runnable() {
@Override
public void run() {
Log.e("Junli","task has been executed");
}
};
mScheduledExecutor.scheduleAtFixedRate(runnable,1000,3000, TimeUnit.MILLISECONDS);
你会发现ScheduledExecutorService和Timer有类似之处,不错,他们都可以用来支持周期性任务的执行,而且ScheduleExecutorService相比TimerTask有更多的优点:
- TimerTask 是基于绝对时间的,因此任务系统对时钟的改变是敏感的。而Scheduled…是基于相对时间的
- 所有的TimerTask只有一个线程TimerThread来执行,因此同一时刻只有一个TimerTask在执行。 而且如果一个timertask执行时间很长,超过了短任务的间隔,将会导致短任务延迟执行或者被废弃,而Scheduled会为每个任务开启一个线程则基本不会有这种问题
- 任何一个TimerTask的执行异常都会导致Timer终止所有任务。而Scheduled…在其中一个任务发生异常时会退出执行线程,但同时会有新的线程补充进来进行执行。