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

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

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

线程结果获取

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

 /*
* 设计可以返回运行结果的线程
* 定义一个线程读取文件内容, 使用字符串存取结果并返回主线程
*/
public class ReturnDigestTest extends Thread{
//定义文件名
private String fileName;
//定义一个字符串对象result, 用于存取线程执行结果
private String result; public ReturnDigestTest(String fileName) {
this.fileName = fileName;
}
//run方法中读取本目录下文件, 并存储至result
@Override
public void run() {
try (FileInputStream fis = new FileInputStream(fileName)){
byte[] buffer = new byte[];
int hasRead = ;
while ((hasRead = fis.read(buffer)) > ) {
result = new String(buffer, , hasRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
//定义返回result结果的方法
public String getResult() {
return result;
}
public static void main(String[] args) throws InterruptedException {
//测试, 在子线程中执行读取文件, 主线程返回
ReturnDigestTest returnDigestTest = new ReturnDigestTest("test.txt");
returnDigestTest.start();
//以下结果返回null. 因为getResult方法执行的时候, 子线程可能还没结束
System.out.println(returnDigestTest.getResult());
}
}

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

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

竞态条件

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

轮询

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

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

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

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

接口回调的应用

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

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

实现Runnable的类

 /*
* 使用接口回调, 实现线程执行结果的返回
*/
public class CallbackDigest implements Runnable{
private String fileName;
private String result;
//定义回调方法接口的引用
private CallbackUserInterface cui;
public CallbackDigest(String fileName, CallbackUserInterface cui) {
this.fileName = fileName;
this.cui = cui;
}
@Override
public void run() {
try (FileInputStream fis = new FileInputStream(fileName)){
byte[] buffer = new byte[1024];
int hasRead = 0;
while((hasRead = fis.read(buffer)) > 0) {
result = new String(buffer, 0, hasRead);
}
//通过回调接口引用, 调用了receiveResult方法, 可以在主线程中返回结果.
//此处利用了多态
cui.receiveResult(result, fileName);
} catch (IOException e) {
e.printStackTrace();
}
}
}

回调方法接口

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

回调方法接口实现类

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

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

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

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

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

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

Callable和FutureTask的使用

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

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

基于接口回调详解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. 基于VC++ Win32+CUDA+OpenGL组合与VC++ MFC SDI+CUDA+OpenGL组合两种方案的遥感影像显示:获得的重要结论!

    1.基于VC++ Win32+CUDA+OpenGL组合的遥感影像显示 在该组合方案下,初始化时将OpenGL设置为下面两种方式,效果一样 //设置方式1 glutInitDisplayMode (G ...

  2. XmlSpy / XSD以及验证

    很早以前看过一句话:“XML就象空气”,在企业应用开发中XML是一个重要的数据交换标准.而XSD则可以用来校验XML的数据格式是否正确. 一个典型的XSD文件如下: <?xml version= ...

  3. 矿难让显卡压了那么多货咋办?NV如是说

    在苏州 GTC 开幕的几天前,英伟达刚刚遭遇了一次股价的腰斩. 近来加密货币的热度渐低,受到挖矿热潮照顾许多的英伟达「矿机」销量受到打击,甚至出现了严重的库存危机,加上近来刚刚发的 RTX20 系列显 ...

  4. QT 用listveiw显示图片

    很多的时候需要浏览图片,一般会使用listview 显示图片,接下来我用listview显示图片 代码如下: QStandardItem * s1; QStandardItem * s2; QStan ...

  5. Ubuntu 重新安装声卡驱动

    有的时候ubuntu 的声卡不能用,没有声音也不能使用麦克风,所有很困惑,查看声卡驱动的时候不显示声卡的驱动,所有我们要自己安装声卡驱动, 1.下载驱动包这是比较新的声卡驱动,1.0.20 $ wge ...

  6. 使用git初始化本地仓库并提交到远程分支

    创建本地文件并提交到github远程分支,步骤如下: 1.通过github创建repository,本例中repository名称为maven_demo,工程为maven + spring + myb ...

  7. easyui的datagrid

    datagrid数据的绑定方式: 1)data 后跟数据行的json串 2)url 后跟{"total":,"rows":,"foot":} ...

  8. mock使用中出现的错误

    当出现错误Class mocking requires to have objenesis library in the classpath时,缺少了objenesis库文件...下载objenesi ...

  9. 20155325 Exp2 后门原理与实践

    基础问答 例举你能想到的一个后门进入到你系统中的可能方式? 乱点链接 学电脑小白不正确配置电脑 下载非官网软件 例举你知道的后门如何启动起来(win及linux)的方式? 软件:ncat socat ...

  10. 20155330 《网络对抗》 Exp8 Web基础

    20155330 <网络对抗> Exp8 Web基础 实验问题回答 什么是表单 表单可以收集用户的信息和反馈意见,是网站管理者与浏览者之间沟通的桥梁. 一个表单有三个基本组成部分 表单标签 ...