资讯专栏INFORMATION COLUMN

重拾JAVA线程之获取另一个线程的返回

liuchengxu / 1454人阅读

摘要:它将管理线程的创建销毁和复用,尽最大可能提高线程的使用效率。如果我们在另一个线程中需要使用这个结果,则这个线程会挂起直到另一个线程返回该结果。我们无需再在另一个线程中使用回调函数来处理结果。

前言

Java的多线程机制允许我们将可以并行的任务分配给不同的线程同时完成。但是,如果我们希望在另一个线程的结果之上进行后续操作,我们应该怎么办呢?

注:本文的代码没有经过具体实践的检验,纯属为了展示。如果有任何问题,欢迎指出。

在此之前你需要了解

Thread类

Runnable接口

ExecutorServer, Executors生成的线程池

一个简单的场景

假设我们现在有一个IO操作需要读取一个文件,在读取完成之后我们希望针对读取的字节进行相应的处理。因为IO操作比较耗时,所以我们可能会希望在另一个线程中进行IO操作,从而确保主线程的运行不会出现等待。在这里,我们读取完文件之后会在其所在线程输出其字符流对应的字符串。

//主线程类
public class MainThread {

    public static void main(String[] args){
        performIO();
    }

    public static void performIO(){
        FileReader fileReader = new FileReader(FILENAME);
        Thread thread = new Thread(fileReader);
        thread.start();
    }
}

文件读取类:

public class FileReader implements  Runnable{

    private FileInputStream fileInputStream;
    private String fileName;

    private byte[] content;

    public FileReader(String fileName){
        this.fileName = fileName;
        content = new byte[2048];
    }

    @Override
    public void run() {
        try {
            File file = new File(fileName);
            fileInputStream = new FileInputStream(file);
            int bytesRead = 0;
            while(fileInputStream.available() > 0){
                bytesRead += fileInputStream.read(content, bytesRead, content.length - bytesRead);
            }
            System.out.println(new String(content,0, bytesRead));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
一个错误的例子

假设现在主线程希望针对文件的信息进行操作,那么可能会出现以下的代码:

在子线程中添加get方法返回读取的字符数组:

public class FileReader implements Runnable{

    private FileInputStream fileInputStream;
    private String fileName;

    private byte[] content;

    //添加get方法返回字符数组
    public byte[] getContent(){
        return this.content;
    }
    
    public FileReader(String fileName){
        this.fileName = fileName;
        content = new byte[2048];
    }

    @Override
    public void run() {
        try {
            File file = new File(fileName);
            fileInputStream = new FileInputStream(file);
            int bytesRead = 0;
            while(fileInputStream.available() > 0){
                bytesRead += fileInputStream.read(content, bytesRead, content.length - bytesRead);
            }
            System.out.println(new String(content,0, bytesRead));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

主线程方法中添加读取byte数组的方法:

public class MainThread {

    public static void main(String[] args){
        performIO();
    }

    public static void performIO(){
        FileReader fileReader = new FileReader(FILENAME);
        Thread thread = new Thread(fileReader);
        thread.start();

        //读取内容
        byte[] content = fileReader.getContent();
        System.out.println(content);
    }
}

这段代码不能保证正常运行,原因在于我们无法控制线程的调度。也就是说,在thread.start()语句后,主线程可能依然占有CPU继续执行,而此时获得的content则是null

你搞定了没有啊

主线程可以通过轮询的方式询问IO线程是不是已经完成了操作,如果完成了操作,就读取结果。这里我们需要设置一个标记位来记录IO是否完成。

public class FileReader implements Runnable{

    private FileInputStream fileInputStream;
    private String fileName;

    private byte[] content;
    //新建标记位,初始为false
    public boolean finish;
    
    public byte[] getContent(){
        return this. content;
    }
    public FileReader(String fileName){
        this.fileName = fileName;
        content = new byte[2048];
    }

    @Override
    public void run() {
        try {
            File file = new File(fileName);
            fileInputStream = new FileInputStream(file);
            int bytesRead = 0;
            while(fileInputStream.available() > 0){
                bytesRead += fileInputStream.read(content, bytesRead, content.length - bytesRead);
            }
            finish = true;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

主线程一直轮询IO线程:

public class MainThread {

    public static void main(String[] args){
        performIO();
    }
    
    public static void performIO(){
        FileReader fileReader = new FileReader(FILENAME);
        Thread thread = new Thread(fileReader);
        thread.start();
        while(true){
            if(fileReader.finish){
                System.out.println(new String(fileReader.getContent()));
                break;
            }
        }
    }
}

缺点那是相当的明显,不断的轮询会无谓的消耗CPU。除此以外,一旦IO异常,则标记位永远为false,主线程会陷入死循环。

搞定了告诉我一声啊

要解决这个问题,我们就需要在IO线程完成读取之后,通知主线程该操作已经完成,从而主线程继续运行。这种方法叫做回调。可以用静态方法实现:

public class FileReader implements Runnable{

    private FileInputStream fileInputStream;
    private String fileName;

    private byte[] content;
    
    public FileReader(String fileName){
        this.fileName = fileName;
        content = new byte[2048];
    }

    @Override
    public void run() {
        try {
            File file = new File(fileName);
            fileInputStream = new FileInputStream(file);
            int bytesRead = 0;
            while(fileInputStream.available() > 0){
                bytesRead += fileInputStream.read(content, bytesRead, content.length - bytesRead);
            }
            
            //完成IO后调用主线程的回调函数来通知主线程进行后续的操作
            MainThread.callback(content);
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

主线程方法中定义回调函数:

public class MainThread {

    public static void main(String[] args){
        performIO();
    }

    //在主线程中用静态方法定义回调函数
    public static void callback(byte[] content){
        //do something
        System.out.println(content);
    }
    
    public static void performIO(){
        FileReader fileReader = new FileReader(FILENAME);
        Thread thread = new Thread(fileReader);
        thread.start();
    }
}

这种实现方法的缺点在于MainThread和FileReader类之间的耦合太强了。而且万一我们需要读取多个文件,我们会希望对每一个FileReader有自己的callback函数进行处理。因此我们可以callback将其声明为一般函数,并且让IO线程持有需要回调的方法所在的实例:

public class FileReader implements Runnable{

    private FileInputStream fileInputStream;
    private String fileName;

    private byte[] content;

    //持有回调函数的实例
    private MainThread mainThread;
   
    //传入实例
    public FileReader(String fileName, MainThreand mainThread){
        this.fileName = fileName;
        content = new byte[2048];
        this.mainThread = mainThread;
    }

    @Override
    public void run() {
        try {
            File file = new File(fileName);
            fileInputStream = new FileInputStream(file);
            int bytesRead = 0;
            while(fileInputStream.available() > 0){
                bytesRead += fileInputStream.read(content, bytesRead, content.length - bytesRead);
            }
            System.out.println(new String(content,0, bytesRead));
            mainThread.callback(content);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

主线程方法中添加读取byte数组的方法:

public class MainThread {

    public static void main(String[] args){
        new MainThread().performIO();
    }

    public void callback(byte[] content){
        //do something
    }
    
    //将执行IO变为非静态方法
    public void performIO(){
        FileReader fileReader = new FileReader(FILENAME);
        Thread thread = new Thread(fileReader);
        thread.start();
    }
}
搞定了告诉我们一声啊

有时候可能有多个事件都在监听事件,比如当我点击了Button,我希望后台能够执行查询操作并将结果返回给UI。同时,我还希望将用户的这个操作无论成功与否写入日志线程。因此,我可以写两个回调函数,分别对应于不同的操作。

public interface Callback{
    public void perform(T t);
}

写入日志操作:

public class Log implements Callback{
    public void perform(String s){
        //写入日志
    }
}

IO读取操作

public class FileReader implements Callback{
    public void perform(String s){
        //进行IO操作
    }
}
public class Button{
    private List callables;
    
    public Button(){
        callables = new ArrayList();
    }
    
    public void addCallable(Callable c){
        this.callables.add(c);
    }
    
    public void onClick(){
        for(Callable c : callables){
            c.perform(...);
        }
    }
}
Java7: 行了,别忙活了,朕知道了

Java7提供了非常方便的封装FutureCallablesExecutors来实现之前的回调工作。

之前我们直接将任务交给一个新建的线程来处理。可是如果每次都新建一个线程来处理当前的任务,线程的新建和销毁将会是一大笔开销。因此Java提供了多种类型的线程池来供我们操作。它将管理线程的创建销毁和复用,尽最大可能提高线程的使用效率。

同时Java7提供的Callable接口将自动返回线程运行结束的结果。如果我们在另一个线程中需要使用这个结果,则这个线程会挂起直到另一个线程返回该结果。我们无需再在另一个线程中使用回调函数来处理结果。

假设现在我们想要找到一个数组的最大值。假设该数组容量惊人,因此我们希望新开两个线程分别对数组的前半部分和后半部分计算最大值。然后在主线程中比较两个结果得出结论:

public class ArrayMaxValue {

    public static void main(String[] args){

        Random r = new Random(20);
        int[] array = new int[500];
        for (int i = 0 ; i f1 = executorService.submit(new MaxValue(array, 0, mid));
        Future f2 = executorService.submit(new MaxValue(array, mid, array.length));

        try {
            //主线程将阻塞自己直到两个线程都完成运行,并返回结果
            System.out.println(Math.max(f1.get(), f2.get()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    public class MaxValue implements Callable{
        private final int[] array;
        private final int startIndex;
        private final int endIndex;
        public MaxValue(int[] array, int startIndex, int endIndex){
            this.array = array;
            this.startIndex = startIndex;
            this.endIndex = endIndex;
        }
        @Override
        public Integer call() throws Exception {
            int max = Integer.MIN_VALUE;
            for (int i = startIndex ; i
参考文章
深入理解线程通信


想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76369.html

相关文章

  • 重拾 Java 基础

    摘要:阿里开始招实习,同学问我要不要去申请阿里的实习,我说不去,个人对阿里的印象不好。记得去年阿里给我发了邮件,我很认真地回复,然后他不理我了。 引言 最近好久没有遇到技术瓶颈了,思考得自然少了,每天都是重复性的工作。 阿里开始招实习,同学问我要不要去申请阿里的实习,我说不去,个人对阿里的印象不好。 记得去年阿里给我发了邮件,我很认真地回复,然后他不理我了。(最起码的尊重都没有,就算我菜你起...

    ideaa 评论0 收藏0
  • 重拾Java Network Programming(二)InetAddress

    摘要:前言今天,我将梳理在网络编程中很重要的一个类以及其相关的类。这类主机通常不需要外部互联网服务,仅有主机间相互通讯的需求。可以通过该接口获取所有本地地址,并根据这些地址创建。在这里我们使用阻塞队列实现主线程和打印线程之间的通信。 前言 今天,我将梳理在Java网络编程中很重要的一个类InetAddress以及其相关的类NetworkInterface。在这篇文章中将会涉及: InetA...

    daryl 评论0 收藏0
  • 重拾Java Network Programming(四)URLConnection & C

    摘要:从而一方面减少了响应时间,另一方面减少了服务器的压力。表明响应只能被单个用户缓存,不能作为共享缓存即代理服务器不能缓存它。这种情况称为服务器再验证。否则会返回响应。 前言 本文将根据最近所学的Java网络编程实现一个简单的基于URL的缓存。本文将涉及如下内容: HTTP协议 HTTP协议中与缓存相关的内容 URLConnection 和 HTTPURLConnection Respo...

    魏明 评论0 收藏0
  • 重拾Java Network Programming(四)URLConnection & C

    摘要:从而一方面减少了响应时间,另一方面减少了服务器的压力。表明响应只能被单个用户缓存,不能作为共享缓存即代理服务器不能缓存它。这种情况称为服务器再验证。否则会返回响应。 前言 本文将根据最近所学的Java网络编程实现一个简单的基于URL的缓存。本文将涉及如下内容: HTTP协议 HTTP协议中与缓存相关的内容 URLConnection 和 HTTPURLConnection Respo...

    Guakin_Huang 评论0 收藏0
  • 后台 - 收藏集 - 掘金

    摘要:探究系统登录验证码的实现后端掘金验证码生成类手把手教程后端博客系统第一章掘金转眼间时间就从月份到现在的十一月份了。提供了与标准不同的工作方式我的后端书架后端掘金我的后端书架月前本书架主要针对后端开发与架构。 Spring Boot干货系列总纲 | 掘金技术征文 - 掘金原本地址:Spring Boot干货系列总纲博客地址:http://tengj.top/ 前言 博主16年认识Spin...

    CrazyCodes 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<