Callable接口和FutureTask实现类,是JUC(Java Util Concurrent)包中很重要的两个技术实现,它们使获取多线程运行结果成为可能。它们底层的实现,就是基于接口回调技术。接口回调,许多程序员都耳熟能详,这种技术被广泛应用于异步模块的开发中。它的实现原理并不复杂,但是对初学者来说却并不友好,其中的一个原因是它的使用场景和处理手段,对习惯了单线程开发的初学者来说有点绕。而各种文章或书籍,在解释这一个问题的时候,往往忽视了使用场景,而举一些小明坐车、A和B等等的例子,初学者看完之后往往更迷糊。

本文立足于此,就从多线程中线程结果获取这一需求场景出发,逐步说明接口回调及其在JUC中的应用。

需要了解Java多线程的底层运行机制,可以看这一篇:基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

线程结果获取

习惯了单线程开发的程序员,在异步编程中最难理解的一点,就是如何从线程运行结果返回信息,因为run和start方法本身是没有返回值的。一个基本的方法是,使用一个变量暂存运行结果,另外提供一个公共方法来返回这个变量。实现代码如下:

  1. /*
  2. * 设计可以返回运行结果的线程
  3. * 定义一个线程读取文件内容, 使用字符串存取结果并返回主线程
  4. */
  5. public class ReturnDigestTest extends Thread{
  6. //定义文件名
  7. private String fileName;
  8. //定义一个字符串对象result, 用于存取线程执行结果
  9. private String result;
  10.  
  11. public ReturnDigestTest(String fileName) {
  12. this.fileName = fileName;
  13. }
  14. //run方法中读取本目录下文件, 并存储至result
  15. @Override
  16. public void run() {
  17. try (FileInputStream fis = new FileInputStream(fileName)){
  18. byte[] buffer = new byte[];
  19. int hasRead = ;
  20. while ((hasRead = fis.read(buffer)) > ) {
  21. result = new String(buffer, , hasRead);
  22. }
  23. } catch (IOException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. //定义返回result结果的方法
  28. public String getResult() {
  29. return result;
  30. }
  31. public static void main(String[] args) throws InterruptedException {
  32. //测试, 在子线程中执行读取文件, 主线程返回
  33. ReturnDigestTest returnDigestTest = new ReturnDigestTest("test.txt");
  34. returnDigestTest.start();
  35. //以下结果返回null. 因为getResult方法执行的时候, 子线程可能还没结束
  36. System.out.println(returnDigestTest.getResult());
  37. }
  38. }

运行结果会输出一个null,原因在于读取文件的线程需要执行时间,所以很可能到主线程调用getResult方法的时候,子线程还没结束,结果就为null了。

如果在上面代码第35行,增加TimeUnit.SECONDS.sleep(5); 使主线程休眠5秒钟,你会发现结果正确返回。

竞态条件

在多线程环境下的实际开发场景中,更为常见的情形是,业务线程需要不断循环获取多个线程运行的返回结果。如果按照上述思路开发,那可能的结果为null,也可能导致程序挂起。上述方法是否成功,取决于竞态条件(Race Condition),包括线程数、CPU数量、CPU运算速度、磁盘读取速度、JVM线程调度算法。

轮询

作为对上述方法的一个优化,可以让主线程定期询问返回状态,直到结果非空在进行获取,这就是轮询的思路。沿用上面的例子,只需要把36行修改如下即可:

  1. //使用轮询, 判断线程返回结果是否为null
  2. while (true) {
  3. if (returnDigestTest.getResult() != null) {
  4. System.out.println(returnDigestTest.getResult());
  5. break;
  6. }
  7. }

但是,这个方法仍然不具有普适性,在有些JVM,主线程会占用几乎所有运行时间,而导致子线程无法完成工作。

即便不考虑这个因素,这个方法仍然不理想,它使得CPU运行时间被额外占用了。就好像一个搭公交的小孩,每一站都在问:请问到站了吗?因此,比较理想的方法,是让子线程在它完成任务后,通知主线程,这就是回调方法。

接口回调的应用

在异步编程中,回调的意思是,一个线程在执行中或完毕后,通知另外一个线程,返回一些消息。而接口回调,则是充分利用了Java多态的特征,使用接口作为回调方法的引用。

使用接口回调技术来优化上面的问题,可以设计一个实现Runnable接口的类,一个回调方法的接口,以及一个回调方法接口的实现类(main方法所在类),具体实现如下

实现Runnable的类

  1. /*
  2. * 使用接口回调, 实现线程执行结果的返回
  3. */
  4. public class CallbackDigest implements Runnable{
  5. private String fileName;
  6. private String result;
  7. //定义回调方法接口的引用
  8. private CallbackUserInterface cui;
  9. public CallbackDigest(String fileName, CallbackUserInterface cui) {
  10. this.fileName = fileName;
  11. this.cui = cui;
  12. }
  13. @Override
  14. public void run() {
  15. try (FileInputStream fis = new FileInputStream(fileName)){
  16. byte[] buffer = new byte[1024];
  17. int hasRead = 0;
  18. while((hasRead = fis.read(buffer)) > 0) {
  19. result = new String(buffer, 0, hasRead);
  20. }
  21. //通过回调接口引用, 调用了receiveResult方法, 可以在主线程中返回结果.
  22. //此处利用了多态
  23. cui.receiveResult(result, fileName);
  24. } catch (IOException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }

回调方法接口

  1. public interface CallbackUserInterface {
  2. //只定义了回调方法, 传入一个待读取的文件名参数, 和返回结果
  3. public void receiveResult(String result, String fileName);
  4. }

回调方法接口实现类

  1. public class CallbackTest implements CallbackUserInterface {
  2. //实现回调方法
  3. @Override
  4. public void receiveResult(String result, String fileName) {
  5. System.out.println("文件" + fileName + "的内容是: \n" + result);
  6. }
  7.  
  8. public static void main(String[] args) {
  9. //新建回调接口引用, 指向实现类的对象
  10. CallbackUserInterface test = new CallbackTest();
  11. new Thread(new CallbackDigest("test.txt", test)).start();
  12. }
  13. }

接口回调的技术主要有4个关键点:

1. 发出信息的线程类:定义回调方法接口的引用,在构造方法中初始化。

2. 发出信息的线程类:使用回调方法接口的引用, 来调用回调方法。

3. 收取信息的线程类:实现回调接口,新建回调接口的引用,指向该类的对象。

4. 发出信息的线程类:新建线程类对象是,传入3中新建的实现类对象。

Callable和FutureTask的使用

Callable的底层实现类似于一个回调接口,而FutureTask类似于本例子中读取文件内容的线程实现类。因为FutureTask实现了Runnable接口,所以它的实现类是可以多线程的,而内部就是调用了Callable接口实现类的回调方法,从而实现线程结果的返回机制。demo代码如下:

  1. public class TestCallable implements Callable<Integer>{
  2. //实现Callable并重写call方法作为线程执行体, 并设置返回值1
  3. @Override
  4. public Integer call() throws Exception {
  5. System.out.println("Thread is running...");
  6. Thread.sleep(3000);
  7. return 1;
  8. }
  9.  
  10. public static void main(String[] args) throws InterruptedException, ExecutionException {
  11. //创建Callable实现类的对象
  12. TestCallable tc = new TestCallable();
  13. //创建FutureTask类的对象
  14. FutureTask<Integer> task = new FutureTask<>(tc);
  15. //把FutureTask实现类对象作为target,通过Thread类对象启动线程
  16. new Thread(task).start();
  17. System.out.println("do something else...");
  18. //通过get方法获取返回值
  19. Integer integer = task.get();
  20. System.out.println("The thread running result is :" + integer);
  21. }
  22. }

基于接口回调详解JUC中Callable和FutureTask实现原理的更多相关文章

  1. 详解AQS中的condition源码原理

    摘要:condition用于显式的等待通知,等待过程可以挂起并释放锁,唤醒后重新拿到锁. 本文分享自华为云社区<AQS中的condition源码原理详细分析>,作者:breakDawn. ...

  2. c#接口使用详解

    c#接口使用详解 c#中接口隐式与显示实现 c#中接口可以隐式实现.显示实现,隐式实现更常使用.显示实现较少使用 其区别在于 显示实现避免接口函数签名冲突 显示实现只可以以接口形式调用 显示实现其子类 ...

  3. (数据科学学习手札140)详解geopandas中基于pyogrio的矢量读写引擎

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 大家好我是费老师,前不久我在一篇文章中给大家分享 ...

  4. Java语言Socket接口用法详解

    Socket接口用法详解   在Java中,基于TCP协议实现网络通信的类有两个,在客户端的Socket类和在服务器端的ServerSocket类,ServerSocket类的功能是建立一个Serve ...

  5. 用IDEA详解Spring中的IoC和DI(挺透彻的,点进来看看吧)

    用IDEA详解Spring中的IoC和DI 一.Spring IoC的基本概念 控制反转(IoC)是一个比较抽象的概念,它主要用来消减计算机程序的耦合问题,是Spring框架的核心.依赖注入(DI)是 ...

  6. 【转】详解C#中的反射

    原帖链接点这里:详解C#中的反射   反射(Reflection) 2008年01月02日 星期三 11:21 两个现实中的例子: 1.B超:大家体检的时候大概都做过B超吧,B超可以透过肚皮探测到你内 ...

  7. 详解Webwork中Action 调用的方法

    详解Webwork中Action 调用的方法 从三方面介绍webwork action调用相关知识: 1.Webwork 获取和包装 web 参数 2.这部分框架类关系 3.DefaultAction ...

  8. 【转】详解JavaScript中的this

    ref:http://blog.jobbole.com/39305/ 来源:foocoder 详解JavaScript中的this JavaScript中的this总是让人迷惑,应该是js众所周知的坑 ...

  9. 详解Objective-C中委托和协议

    Objective-C委托和协议本没有任何关系,协议如前所述,就是起到C++中纯虚类的作用,对于“委托”则和协议没有关系,只是我们经常利用协议还实现委托的机制,其实不用协议也完全可以实现委托. AD: ...

随机推荐

  1. unset MAILCHECK

    文件/etc/profile尾部有: unset MAILCHECK 为了解决:每次登陆linux总是提示:you hava a new mail

  2. 网页设计之字体和 CSS 调整

    调整 CSS 首先,我们先来看看问题的源头.CSS 的出现曾是技术的一大进步.你可以用一个集中式的样式表来装饰多个网页.如今很多 Web 开发者都会使用 Bootstrap 这样的框架. 这些框架当然 ...

  3. 如何为一个类型为Color的属性设置默认值

    最近在研究GDI+的时候,用winform来写自定义控件遇到需要为控件的属性设置默认值,但这个属性的类型是System.Drawing.Color.本文只是总结一下各种设置的方法. Example [ ...

  4. win10家庭版安装DockerToolbox-18.03.0-ce

    下载DockerToolbox-18.03.0-ce.exe https://mirrors.aliyun.com/docker-toolbox/windows/docker-toolbox/ 点击安 ...

  5. JAVA框架 Spring 入门

    一.阐述: IoC:我们以前写的框架虽然我们已经进行分层,web.业务层.持久层.但是各个层之间的关系.耦合性比较高,那个层调用其他层的时候,需要new对应层的类的对象,这样的话,我们以后做修改的时候 ...

  6. 贪心算法——字典序最小问题,Saruman‘s Army

    题目描述 Best Cow Line (POJ 3617) 给定长度为N的字符串S,要构造一个长度为N字符串T.T是一个空串,反复执行下列任意操作: 从S的头部删除一个字符,加到T的尾部: 从S的尾部 ...

  7. P1522 牛的旅行 Cow Tours

    题目描述 农民 John的农场里有很多牧区.有的路径连接一些特定的牧区.一片所有连通的牧区称为一个牧场.但是就目前而言,你能看到至少有两个牧区通过任何路径都不连通.这样,Farmer John就有多个 ...

  8. jqgrid 编辑行、新增行、删除行、保存行

    编辑行:$("#jqGrid").jqGrid('editRow', rowKey); 删除行:$("#jqGrid").delGridRow(rowKey); ...

  9. STS-新建spring mvc项目

    引入响应的jar包解决报错: 由于国内的网络限制,下载会较慢.使用之前可自行更换maven的镜像路径,越近越好.

  10. 使用Novell.Directory.Ldap.NETStandard在.NET Core中验证AD域账号

    Novell.Directory.Ldap.NETStandard是一个在.NET Core中,既支持Windows平台,又支持Linux平台,进行Windows AD域操作的Nuget包. 首先我们 ...