Java多线程编程简明教程(1) - Future模式与AsyncTask

简介: 最近一直在写多线程的东西,也看了不少教程,觉得材料的组织有可以改进之处。主要原因是对于时间要求太高,要想写出正确的代码,基本上得把一本书学完才行。 于是我尝试可以改进一下,是否可以先讲些安全的,可以短平快的,最后再讲大块头的。

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
    }
}

从上面的例子代码可以看到,都是填空题,逻辑很简单:

  1. 需要的返回值是什么类型,就实现Callable<类型>接口,然后在call()方法里实现后台逻辑。
  2. 用Callable接口的实现类去构造一个FutureTask对象。
  3. 构建ExecutorService对象,决定用哪种线程池。
  4. 提交任务。
  5. 让后台任务在空中飞一会儿,前台该干嘛干嘛。
  6. 前台的事儿忙完了,去读取后台的结果。

就这么简单,线程是什么?锁是什么?如何防止竞态和死锁,这些统统用不到。

线程池

关于线程池,我们只讲三种最基本的,其余的细节后面详细讲。
这三种是:只有一个线程,固定数目的,按需分配的.

分别对应了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    }
目录
相关文章
|
2天前
|
JavaScript NoSQL Java
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
139 96
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
|
23天前
|
消息中间件 Java 数据库
自研Java框架 Sunrays-Framework使用教程「博客之星」
### Sunrays-Framework:助力高效开发的Java微服务框架 **Sunrays-Framework** 是一款基于 Spring Boot 构建的高效微服务开发框架,深度融合了 Spring Cloud 生态中的核心技术组件。它旨在简化数据访问、缓存管理、消息队列、文件存储等常见开发任务,帮助开发者快速构建高质量的企业级应用。 #### 核心功能 - **MyBatis-Plus**:简化数据访问层开发,提供强大的 CRUD 操作和分页功能。 - **Redis**:实现高性能缓存和分布式锁,提升系统响应速度。 - **RabbitMQ**:可靠的消息队列支持,适用于异步
自研Java框架 Sunrays-Framework使用教程「博客之星」
|
2天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
29 13
|
3天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
25天前
|
Java 数据库连接 数据处理
探究Java异常处理【保姆级教程】
Java 异常处理是确保程序稳健运行的关键机制。它通过捕获和处理运行时错误,避免程序崩溃。Java 的异常体系以 `Throwable` 为基础,分为 `Error` 和 `Exception`。前者表示严重错误,后者可细分为受检和非受检异常。常见的异常处理方式包括 `try-catch-finally`、`throws` 和 `throw` 关键字。此外,还可以自定义异常类以满足特定需求。最佳实践包括捕获具体异常、合理使用 `finally` 块和谨慎抛出异常。掌握这些技巧能显著提升程序的健壮性和可靠性。
44 4
|
25天前
|
存储 移动开发 算法
【潜意识Java】Java基础教程:从零开始的学习之旅
本文介绍了 Java 编程语言的基础知识,涵盖从简介、程序结构到面向对象编程的核心概念。首先,Java 是一种高级、跨平台的面向对象语言,支持“一次编写,到处运行”。接着,文章详细讲解了 Java 程序的基本结构,包括包声明、导入语句、类声明和 main 方法。随后,深入探讨了基础语法,如数据类型、变量、控制结构、方法和数组。此外,还介绍了面向对象编程的关键概念,例如类与对象、继承和多态。最后,针对常见的编程错误提供了调试技巧,并总结了学习 Java 的重要性和方法。适合初学者逐步掌握 Java 编程。
50 1
|
25天前
|
前端开发 Java 开发工具
Git使用教程-将idea本地Java等文件配置到gitte上【保姆级教程】
本内容详细介绍了使用Git进行版本控制的全过程,涵盖从本地仓库创建到远程仓库配置,以及最终推送代码至远程仓库的步骤。
29 0
|
6天前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
2月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
79 1
|
4月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
78 1