【本文转载于 线程之从线程返回信息

习惯了传统单线程过程式模型的程序员在转向多线程环境时,最难掌握的一点就是如何从线程返回信息。我们再拿前一blog中的例子为例,不再简单地显示SHA-256摘要,摘要线程需要把摘要返回给执行主线程。大多数人的第一个反应就是把结果存储在一个字段中,再提供一个获取方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package o1;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.DigestInputStream;
import java.security.MessageDigest;
public class ReturnDigest extends Thread {
    private String filename;
    private byte[] digest;
    public ReturnDigest(String filename){
        this.filename = filename;
    }
    @Override
    public void run() {
        try {
            FileInputStream in = new FileInputStream(filename);
            MessageDigest sha = MessageDigest.getInstance("SHA-256");
            DigestInputStream din = new DigestInputStream(in,sha);
            while(din.read() != -1);    //读取整个文件
            din.close();
            digest = sha.digest();
        catch (IOException e1) {
            e1.printStackTrace();
        catch (Exception e2){
            e2.printStackTrace();
        }
    }
    public byte[] getDigest() {
        return digest;
    }
}

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package o1;
import javax.xml.bind.DatatypeConverter;
public class ReturnDigestUserInterface {
    public static void main(String[] args) {
        String filename = "/home/fuhd/work/workspace/scala/t1/src/t1/MyTest.scala";
        ReturnDigest dr = new ReturnDigest(filename);
        dr.start();
        //现在显示结果
        StringBuilder result = new StringBuilder(filename);
        result.append(": ");
        byte[] digest = dr.getDigest();
        result.append(DatatypeConverter.printHexBinary(digest));
        System.out.println(result);
    }
}

ReturnDigest类把计算结果存储在私有字段digest中,可以通过getDigest()来访问。ReturnDigestUserInterface中的main()方法启动一个新的ReturnDigest线程,然后试图使用getDigest()获取结果。不过,当你运行这个程序时,结果却不像你期望的那样:

?
1
2
3
4
Exception in thread "main" java.lang.NullPointerException
    at javax.xml.bind.DatatypeConverterImpl.printHexBinary(DatatypeConverterImpl.java:475)
    at javax.xml.bind.DatatypeConverter.printHexBinary(DatatypeConverter.java:626)
    at o1.ReturnDigestUserInterface.main(ReturnDigestUserInterface.java:14)

问题在于,主程序会在线程有机会初始化摘要之前就获取并使用摘要。dr.start()启动的计算可能在main()方法调用dr.getDigest()之前结束,也可能还没有结束。如果没有结束,dr.getDigest()则会返回null,第一次尝试访问digest是会抛出一个NullPointerException异常。

轮询

大多数新手采用的解决方案是,让获取方法返回一个标志值(或者可能抛出一个异常),直到设置了结果字段为止。然后主线程定期询问获取方法,查看是否返回了标志之外的值。这个例子中,表示要重复地测试digest是否为空,只有不为空才使用。示例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package o1;
import javax.xml.bind.DatatypeConverter;
public class ReturnDigestUserInterface {
    public static void main(String[] args) {
        String filename = "/home/fuhd/work/workspace/scala/t1/src/t1/MyTest.scala";
        ReturnDigest dr = new ReturnDigest(filename);
        dr.start();
        while(true){
            //现在显示结果
            byte[] digest = dr.getDigest();
            if(digest != null){
                StringBuilder result = new StringBuilder(filename);
                result.append(": ");
                result.append(DatatypeConverter.printHexBinary(digest));
                System.out.println(result);
                break;
            }
        }
    }
}

这个解决方案是可行的。它会给出正确的答案。不过,它做了大量不需要做的工作。更糟糕的是,这个解决方案不能保证一定能工作。在有些虚拟机上,主线程会占用所有可用的时间,而没有给具体的工作线程留出任何时间。主线程太忙于检查工作的完成情况,以至于没有时间来具体完成任务!显然这不是一个好方法。

回调

事实上,还有一种更简单有效的方法来解决这个问题。这个方法的技巧在于,不是在主程序中重复地询问每个ReturnDigest线程是否结束,而是让线程告诉主线程它何时结束。这是通过调用主类(即启动这个线程的类)中的一个方法来做到的。这被称为回调(callback),因为线程在完成时反过来调用其创建者。这样一来,主程序就可以在等待线程结束期间休息,而不会占用运行线程的时间。当线程run()方法接近结束时,要做的最后一件事情就是基于结果调用主程序中的一个已知方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package o1;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.DigestInputStream;
import java.security.MessageDigest;
public class CallbackDigest implements Runnable {
    private String filename;
    public CallbackDigest(String filename){
        this.filename = filename;
    }
    @Override
    public void run() {
        try {
            FileInputStream in = new FileInputStream(filename);
            MessageDigest sha = MessageDigest.getInstance("SHA-256");
            DigestInputStream din = new DigestInputStream(in,sha);
            while(din.read() != -1);    //读取整个文件
            din.close();
            byte[] digest = sha.digest();
            CallbackDigestUserInterface.receiveDigest(digest,filename);
        catch (IOException e1) {
            e1.printStackTrace();
        catch (Exception e2){
            e2.printStackTrace();
        }
    }
}

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package o1;
import javax.xml.bind.DatatypeConverter;
public class CallbackDigestUserInterface {
    public static void main(String[] args) {
        String filename = "/home/fuhd/work/workspace/scala/t1/src/t1/MyTest.scala";
        CallbackDigest dr = new CallbackDigest(filename);
        Thread thread = new Thread(dr);
        thread.start();
    }
    public static void receiveDigest(byte[] digest,String filename){
        StringBuilder result = new StringBuilder(filename);
        result.append(": ");
        result.append(DatatypeConverter.printHexBinary(digest));
        System.out.println(result);
    }
}

示例中使用静态方法完成回调,这样CallbackDigest只需要知道CallackDigestUserInterface中要调用的方法名。不过,回调实例方法也不会太难(而且回调实例方法更为常见)。这种情况下,进行回调的类必须有其回调对象的一个引用。通常情况下,这个引用通过线程构造函数来提供。当run()方法接近结束时,要做的最后一件事情就是调用回调对象的实例方法来传递结果。如例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package o1;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.DigestInputStream;
import java.security.MessageDigest;
public class InstanceCallbackDigest implements Runnable {
    private String filename;
    private InstanceCallbackDigestUserInterface callback;
    public InstanceCallbackDigest(String filename,InstanceCallbackDigestUserInterface callback){
        this.filename = filename;
        this.callback = callback;
    }
    @Override
    public void run() {
        try {
            FileInputStream in = new FileInputStream(filename);
            MessageDigest sha = MessageDigest.getInstance("SHA-256");
            DigestInputStream din = new DigestInputStream(in,sha);
            while(din.read() != -1);    //读取整个文件
            din.close();
            byte[] digest = sha.digest();
            callback.receiveDigest(digest, filename);
        catch (IOException e1) {
            e1.printStackTrace();
        catch (Exception e2){
            e2.printStackTrace();
        }
    }
}

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package o1;
import javax.xml.bind.DatatypeConverter;
public class InstanceCallbackDigestUserInterface {
    public static void main(String[] args) {
        String filename = "/home/fuhd/work/workspace/scala/t1/src/t1/MyTest.scala";
        InstanceCallbackDigestUserInterface main = new InstanceCallbackDigestUserInterface();
        InstanceCallbackDigest dr = new InstanceCallbackDigest(filename,main);
        Thread thread = new Thread(dr);
        thread.start();
    }
    public void receiveDigest(byte[] digest,String filename){
        StringBuilder result = new StringBuilder(filename);
        result.append(": ");
        result.append(DatatypeConverter.printHexBinary(digest));
        System.out.println(result);
    }
}

相比于轮询机制,回调机制的第一个优点是不会浪费那么多CPU周期。但更重要的优点是回调更灵活,可以处理涉及更多线程,对象和类的更复杂的情况。例如,如果有多个对象对线程的计算结果感兴趣,那么线程可以保存一个要回调的对象列表。特定的对象可以通过调用Thread或Runnable类的一个方法把自己添加到这个列表中来完成注册,表示自己对计算结果很感觉兴趣。如果有多个类的实例对结果感兴趣,可以定义一个新的interface(接口),所有这些类都要实现这个新接口。这个interface(接口)将声明回调方法。如果你对此有种似曾相识的感觉,没错,这就是Swing,AWT中处理事件的机制。这种机制有一个更一般的名字:观察者(Observer)设计模式。

Future,Callable和Executor

java5引入了多线程编程的一个新方法,通过隐藏细节可以更容易地处理回调。不再是直接创建一个线程,你要创建一个ExecutorService,它会根据需要为你创建线程。可以向ExecutorService提交Callable任务,对于每个Callable任务,会分别得到一个Future。之后可以向Future请求得到任务的结果。如果结果已经准备就绪,就会立即得到这个结果。如果还没有准备好,轮询线程会阻塞,直到结果准备就绪。这种做法的好处是,你可以创建很多不同的线程,然后按你需要的顺序得到你需要的答案。

例如,假设你要找出一个很大的数字数组中的最大值。如果采用最原始的方法实现,需析时间为O(n),其中n是数组中的元素个数。不过,如果可以将这个工作分解到多个线程,每个线程分别在一个单独的内核上运行,这样就会快得多。如例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package o1;
import java.util.concurrent.Callable;
public class FindMaxTask implements Callable<Integer> {
    private int[] data;
    private int start;
    private int end;
    public FindMaxTask(int[] data,int start,int end){
        this.data = data;
        this.start = start;
        this.end = end;
    }
    @Override
    public Integer call() throws Exception {
        int max = Integer.MIN_VALUE;
        for(int i = start;i < end; i++){
            if(data[i] > max) max = data[i];
        }
        return max;
    }
}

Callable接口定义了一个call()方法,它可以返回任意的类型。尽管可以直接调用call()方法,但这并不是它的本来目的。实际上,你要把Callable对象提交给一个Executor,它会为每个Callable对象创建一个线程(Executor还可以使用其他策略,例如,它可以使用一个线程按顺序调用这些callable,不过对于这个问题来说,每个callable分别对应一个线程是一个很好的策略)。示例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package o1;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class MultithreadedMaxFinder {
    public int max(int[] data,ExecutorService service) throws InterruptedException,ExecutionException{
        if(data.length == 1){
            return data[0];
        }else if(data.length == 0){
            throw new IllegalArgumentException();
        }
        //将任务分解为两部分
        FindMaxTask task1 = new FindMaxTask(data,0,data.length/2);
        FindMaxTask task2 = new FindMaxTask(data,data.length/2,data.length);
        //创建2个线程
        Future<Integer> f1 = service.submit(task1);
        Future<Integer> f2 = service.submit( task2);
        return Math.max(f1.get(), f2.get());
    }
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        try {
            MultithreadedMaxFinder m = new MultithreadedMaxFinder();
            int[] numArr = {345,213,45,675,127,478,456};
            System.out.println(m.max(numArr,service));
        catch (InterruptedException e) {
            e.printStackTrace();
        catch (ExecutionException e) {
            e.printStackTrace();
        catch (Exception e){
            e.printStackTrace();
        }finally{
            service.shutdown();
        }
    }
}

这里会同时搜索两个子数组,所以对于合适的硬件和规模很大的输入。这个程序运行的速度几乎可以达到原来的两倍。不仅如此,与先找出数组前一半的最大值再找出数组后一半的最大值的做法相比,这个代码几乎同样简单和直接,而不用担心线程或异步性。不过,这里有一个重要的区别。调用f1.get()时,这个方法会阻塞,等待第一个FindMaxTask完成。只有当第一个FindMaxTask完成时,才会调用f2.get()。也有可能第二个线程已经结束,在这种情况下,结果值会直接返回,但是如果第二个线程还没有结束,同样的,也会等待这个线程完成。一旦两个线程都结束,将比较它们的结果,并返回最大值。

Future是一种非常方便的做法,可以启动多个线程来处理一个问题的不同部分,然后等待它们全部都结束之后再继续。

线程之从线程返回信息相关推荐

  1. 获取进程或线程的ID以及句柄信息

    先介绍一下创建线程或进程的时候是可以得到相应的ID以及句柄信息的. BOOL CreateProcess ( LPCTSTR lpApplicationName, LPTSTR lpCommandLi ...

  2. java文件对比7,一个线程读一个线程写、返回给前端进度条数据

    java文件对比 controller Service Serviceimpl 读取文件多线程工具类 对比文件多线程工具类 控制台结果 返回结果 进度条结果 个人总结 这个其实写的是有点问题的,想的是 ...

  3. C#线程池ThreadPool.QueueUserWorkItem接收线程执行的方法返回值

    最近在项目中需要用到多线程,考虑了一番,选择了ThreadPool,我的需求是要拿到线程执行方法的返回值, 但是ThreadPool.QueueUserWorkItem的回调方法默认是没有返回值的,搜 ...

  4. Spring线程池异步传递MDC信息

    目录 1. 什么是MDC 2. 引入MDC打印步骤 2.1 pom依赖 2.2 log4j2打印日志配置文件 3 步骤演示 3.1 单线程业务使用示例 postman查询示例 查询代码 查询日志 3. ...

  5. ReentrantLock+线程池+同步+线程锁

    1.并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行. 2)可见性 可见性指多个线程操作一个共享变量时,其中一个线程对变量 ...

  6. linux 线程--内核线程、用户线程实现方法

    Linux上进程分3种,内核线程(或者叫核心进程).用户进程.用户线程 内核线程拥有 进程描述符.PID.进程正文段.核心堆栈 当和用户进程拥有相同的static_prio 时,内核线程有机会得到更多 ...

  7. 【C++ 语言】线程 ( 线程创建方法 | 线程标识符 | 线程属性 | 线程属性初始化 | 线程属性销毁 | 分离线程 | 线程调度策略 | 线程优先级 | 线程等待 )

    文章目录 I 线程创建方法 II 线程执行函数 III 线程标识符 IV 线程属性 V 线程属性 1 ( 分离线程 | 非分离线程 ) VI 线程属性 2 ( 线程调度策略 ) VII 线程属性 3 ...

  8. 并发编程-11线程安全策略之线程封闭

    文章目录 脑图 概述 线程封闭的三种方式 示例 堆栈封闭 ThreadLocal Step1. ThreadLocal操作类 Step2. 自定义过滤器 Step3. 注册拦截器,配置拦截规则 Ste ...

  9. 由浅入深理解Java线程池及线程池的如何使用

    前言 多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担.线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致Out of Memory ...

最新文章

  1. Java 成员变量与局部变量
  2. ant models 内获取 url 的参数传递到组件
  3. PhoenixGo战胜绝艺,腾讯包揽AI围棋大赛前两名
  4. Go 语言编程 — Profiling 性能分析
  5. 成功解决AttributeError: module 'string' has no attribute 'find'
  6. Android处理崩溃的一些实践
  7. Memcache-No.03 Memcache相关安装、部署、启动、监控
  8. Win10系统自带输入法怎么设置
  9. java 中break如何跳出多层循环(包含二层循环)
  10. PHP ceil()函数
  11. 集合类型及其操作(复习)
  12. L1-022 奇偶分家 (10 分) — 团体程序设计天梯赛
  13. 网站漏洞扫描工具AWVS_v13下载和安装
  14. Hexo+阿里云服务器搭建属于自己的博客
  15. 用户登录项目第二期——HTML登录页面实现
  16. 对象流水线 -- 工厂模式介绍 使用案例及代码演示
  17. 如何快速提取视频中的文案?
  18. linux 变量引用 和 变量的自动类型转换 c++,C++能不能让编译器自动推导变量类型吗...
  19. 怎么在电脑上玩电击文库零境交错 电击文库零境交错电脑版教程
  20. uedit如何连接本机linux虚拟机,实现文件交互

热门文章

  1. Media Player网页播放音频,视频,图片总汇
  2. [Hadoop in China 2011] Facebook Message在HBase基础上的应用
  3. maven仓库阿里云镜像配置
  4. 在cxf中使用配置避免增加字段导致客户端必须更新、同步实体属性的问题
  5. Java设计模式圣经连载(05)-代理模式
  6. 从单体应用转为分布式系统:来自Deliveroo的实践
  7. UVA 11020 - Efficient Solutions(set)
  8. 表面积最小(POJ3536)
  9. Sql Server导出表结构Excel
  10. 多行文本框拖动问题解决