Java多线程编程简明教程
缘起
关于多线程编程的教程汗牛充栋了,专门讲Java并发开发的书籍也是相当丰富了。
我们举个例子,典型的一本Java多线程开发的教材是这样写的,上来就是介绍如何创建线程,然后再讲线程安全,线程之间如何做同步和通信,接着才是线程池和执行器,最后是线程安全的数据结构。
这样写当然从技术上讲是没问题的,不过问题在于,门槛太高了。假如读者的时间短,只看完创建线程这一章就开始照猫画虎写多线程的代码,于是,多线程竞态的问题在那里等着呢。假如他多再多看一些,学会了线程的同步互斥,而且也解决了竞态和死锁的问题,他仍然不会想到要使用线程池,在使用容器的时候也不免会遇到一些坑。
但是,真的要经过这么专业的训练才可以写一个简单的多任务的么?比如对于很多任务来说,只是需要静静地在后台去执行一个任务而己,在运行过程中,并不需要同步访问共享,最后只是返回一个结果就好了。对于一些更复杂的问题,可以采用分治法处理,规划得当的话并不会出现访问冲突。对于前面说的第一种情况,使用JDK 5引入的Future模式已经足够了,而对于后一种的情况,使用JDK 7中引入的Fork-Join框架就可以很好的解决。这两种模式都是线程安全的,根本不共享状态有什么不安全的呢。而学会了这两个模式之后,习惯成自然地,将来再学习线程也会习惯性地使用线程池,不会引入创建和销毁大量线程的消耗。
学会了这两个模式之后,我们进一步再学习如何避免共享状态,如何封闭状态。如何通过使用安全的数据结构进行通信去共享状态。
如果以上都实在解决不了问题,最后再老老实实地学习Java的内存模型,再去实现传统上一开始就讲的创建线程这一套机制。
按照传统的思路,讲到创建线程的时候,初学者对于Java内存模型,对于在多线程情况下的数据结构,对于线程池这些完全没有概念,一切都按照单任务的经验来写代码,于是造成了很多问题而不知。而现在是在不满足前面的常用模式的情况下才选择这条路,读者已经非常清楚他在做什么,要承担什么样的风险,以及如何降低这个风险。
最后,他山之石,可以攻玉。学习借鉴其他语言和模型的成功经验和教训,可能比只懂线程的同步和互斥有更广的思路。
很多书籍的编排已经透出了不少新意,比如清英大牛的《Java并发编程的艺术》,在讲线程之前,先讲一章庞大的Java内存模型。这样在学习线程的时候,对于可见不可见,执行顺序等等已经有很清楚的概念了,起码知道跟单线程时不一样了,这就避免了很多问题。
我的教程希望能够,不管在哪一部分看完之后暂时中断了,学到的体系也是相对完整的,力争避免会了线程,但是写出一堆问题的代码。哪怕学习了如何危险还不知道如何写,也比不知道危险要好,至少知道要学完才能用。
Future模式
Future模式5分钟教程
Future模式用于的场景是,不急于马上就拿到结果的任务,可以放到后台先做着,然后主线程继续忙别的去。等主线程需要用后台任务的结果了,再去从Future去拿结果。如果Future任务已经做完了,当然皆大欢喜,马上使用结果,代码逻辑继续往下跑;如果还没做完,至少等待的时间比一直干等着强。总之,稳赚不赔,何乐而不为。如果这时候那个结果已经不重要了,取消了也没问题。
这里面没有竞态,没有共享哪来的竞态。同样,不需要懂锁是什么东西。
使用Future模式需要4个步骤:
1. 构造一个Callable接口的实现,在其中写在后台要实现的逻辑
2. 构造一个线程池执行器,提交执行
3. 主线程继续忙自己的
4. 想要用这个Future的时候,调用FutureTask对象的get()方法去获取值
我们举个简单的例子来看一下:
public class AsyncTaskSimple {
public static class Result implements Callable<String>{
@Override
public String call() throws Exception {
return doRealLogic();
}
private String doRealLogic(){
//Here to do the background logic
return new String("Done");
}
}
public static void main(String[] args) {
FutureTask<String> future = new FutureTask<String>(new Result());
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(future);
someThingToDo();
try {
String s = future.get();
System.out.println("The result is:"+s);
}catch (InterruptedException e){
//Deal with InterruptedExcpeiotn
}catch(ExecutionException ee){
//Deal with ExecutionException
}
}
private static void someThingToDo(){
//Main thread logic
}
}
从上面的例子代码可以看到,都是填空题,逻辑很简单:
- 需要的返回值是什么类型,就实现Callable<类型>接口,然后在call()方法里实现后台逻辑。
- 用Callable接口的实现类去构造一个FutureTask对象。
- 构建ExecutorService对象,决定用哪种线程池。
- 提交任务。
- 让后台任务在空中飞一会儿,前台该干嘛干嘛。
- 前台的事儿忙完了,去读取后台的结果。
就这么简单,线程是什么?锁是什么?如何防止竞态和死锁,这些统统用不到。
线程池
关于线程池,我们只讲三种最基本的,其余的细节后面详细讲。
这三种是:只有一个线程,固定数目的,按需分配的.
分别对应了Executors类的三个静态方法:
* public static ExecutorService newSingleThreadExecutor ():就建一个,大家排队按顺序来
* public static ExecutorService newFixedThreadPool (int nThreads):定义固定nThreads个大小的线程池。
* public static ExecutorService newCachedThreadPool ():最省事了,完全由系统自己管理。能重用就重用,不能重用就建新的线程。
好,Future模式就讲完了,大家可以上手写代码了。无锁、无竞态、使用了线程池。从创建线程开始学的同学们还要学习Java内存模型,如何同步,如何互斥,如何避免死锁等等,这些我们都暂时不用学,也不需要线程安全的数据结构,因为传进去的参数和返回的结果都是只读的,没有多线程去抢着写它们。
揭开Android AsyncTask的面纱
下面我们说说Android中叫做AsyncTask的东西,其实就是在Future上做的一个简要封装。
我们一起看下,有Android经验的同学正好借已有的经验来加深一下印象。
定义
public abstract class AsyncTask<Params, Progress, Result> {
这个类有三个泛型参数,Params, Progress, Result,这里先记住,我们后面会看到它们各起什么作用。
构造方法
我们把无关的代码省略掉,核心逻辑一共就两句话,一句是调用子类的doInBackground方法,一句是调用postResult方法将返回值返回。
288 public AsyncTask() {
289 mWorker = new WorkerRunnable<Params, Result>() {
290 public Result call() throws Exception {
...
295 Result result = doInBackground(mParams);
...
297 return postResult(result);
298 }
299 };
这个WorkerRunnable是实现了Callable的一个抽象类.
private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
Params[] mParams;
}
通过这一步,完成了我们前面学习的Callable接口对象的创建。按照我们前面学习的,下一步该构造FutureTask对象了,AsyncTask的构造方法就是这么做的!用刚才生成的mWorker对象去构造mFuture对象。
这里与前面所讲的有一点不同的是,前面我们是处理完了由主线程主动查询Future是否结束,而AsyncTask是在任务结束之后,也就是重载done()方法,在这里面再去调用get()方法去获取Future的值,再将其主动发出去。
301 mFuture = new FutureTask<Result>(mWorker) {
302 @Override
303 protected void done() {
304 try {
305 postResultIfNotInvoked(get());
306 } catch (InterruptedException e) {
307 android.util.Log.w(LOG_TAG, e);
308 } catch (ExecutionException e) {
309 throw new RuntimeException("An error occurred while executing doInBackground()",
310 e.getCause());
311 } catch (CancellationException e) {
312 postResultIfNotInvoked(null);
313 }
314 }
315 };
316 }
上面的调用的get()方法一点新意也没有,就是mFuture.get()的简单封装,我们看代码:
497 public final Result get() throws InterruptedException, ExecutionException {
498 return mFuture.get();
499 }
只有到了将结果通知出来这个过程是跟Android相关的,用到了Android的消息队列。
318 private void postResultIfNotInvoked(Result result) {
319 final boolean wasTaskInvoked = mTaskInvoked.get();
320 if (!wasTaskInvoked) {
321 postResult(result);
322 }
323 }
324
325 private Result postResult(Result result) {
326 @SuppressWarnings("unchecked")
327 Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
328 new AsyncTaskResult<Result>(this, result));
329 message.sendToTarget();
330 return result;
331 }
executeOnExecutor
我们再看看mFuture是何时被执行的,一句话,是在executeOnExecutor中。这个方法是在主线程中运行的,所以可以先安全地运行onPreExecute(),而onPostExecute()则要是在Handler里面处理了。
587 @MainThread
588 public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
589 Params... params) {
...
604 onPreExecute();
605
606 mWorker.mParams = params;
607 exec.execute(mFuture);
608
609 return this;
610 }
onPostExecute和通知进度
这个都是在Handler中实现的了,结束了就onPostExecute,还没完就通知下进度。
656 private static class InternalHandler extends Handler {
657 public InternalHandler() {
658 super(Looper.getMainLooper());
659 }
660
661 @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
662 @Override
663 public void handleMessage(Message msg) {
664 AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
665 switch (msg.what) {
666 case MESSAGE_POST_RESULT:
667 // There is only one result
668 result.mTask.finish(result.mData[0]);
669 break;
670 case MESSAGE_POST_PROGRESS:
671 result.mTask.onProgressUpdate(result.mData);
672 break;
673 }
674 }
675 }
finish方法根据是否cancel决定调用哪一个,正常情况下是onPostExecute
647 private void finish(Result result) {
648 if (isCancelled()) {
649 onCancelled(result);
650 } else {
651 onPostExecute(result);
652 }
653 mStatus = Status.FINISHED;
654 }