zoukankan      html  css  js  c++  java
  • 基于接口回调详解JUC中Callable和FutureTask实现原理

    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[1024];
    19             int hasRead = 0;
    20             while ((hasRead = fis.read(buffer)) > 0) {
    21                 result = new String(buffer, 0, 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 + "的内容是: 
    " + 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 }
  • 相关阅读:
    一文解读AI芯片之间的战争 (转)
    一文解读ARM架构 (转)
    一文解读云计算 (转)
    一文解读裸金属云 (转)
    一文解读发布策略 (转)
    C#使用OracleDataReader返回DataTable
    centos8平台上php7.4的生产环境配置
    centos8安装php7.4
    centos8安装java jdk 13
    docker的常用操作之二:docker内无法解析dns之firewalld设置等
  • 原文地址:https://www.cnblogs.com/leoliu168/p/9938718.html
Copyright © 2011-2022 走看看