Future,Callable,Executor

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

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

    Callable接口定义了一个call()方法,它可以返回任意的类型,示例是一个Callable,它会采用最明显的方式查找数组的一个分段中的最大值。

package wxmdemo.com.practisejavanet;

import java.util.concurrent.Callable;

/**
 * Created by admin on 2017/7/29.
 */
public class FindMaxTask implements Callable{

    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;
    }
}

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

package wxmdemo.com.practisejavanet;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * Created by admin on 2017/7/29.
 */
public class MultithreadedMaxFinder {
    
    public static int max(int [] data) 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);
        
        //创建两个线程
        ExecutorService service = Executors.newFixedThreadPool(2);
        
        Future<Integer> future1 = service.submit(task1);
        Future<Integer> future2 = service.submit(task2);

        return Math.max(future1.get(), future2.get());
    }
}

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


    Future是一种非常方便的做法,可以启动多个线程来处理一个问题的不同部分,然后等待它们全部都结束之后再继续。executor和executor服务允许你用不同的策略将任务分配给不同的线程。这个例子只使用了两个线程,不过完全可以使用更多的线程,并重用这些线程来完成多个任务。只要你能把任务分解到适当独立的部分,Executor就可以隐藏异步性的很多细节。


在电脑上经过测试,

在100个随机int值的数组找最大值的时候,一个一个遍历用时6000纳秒,分两个线程用时2000w纳秒

在100w个随机int值的数组找最大值的时候,一个一个遍历用时500w纳秒,分两个线程用时1700w纳秒

在1亿个随机int值的数组找最大值的时候,一个一个遍历用时6000w纳秒,分两个线程用时6000w纳秒

可以大概看出来,一个个遍历在数据量变大的时候耗时明显增加,而两个线程情况下只有在1亿数据量的时候能感觉到有稍大变化,可能在两个线程跑的过程中,创建对象等操作比较耗时(前两个测试结果可以看出来100个和100w个耗时没差多少,可能时间都花在创建对象、调用方法上)。所以小数据量的时候,一个个遍历有优势,大数据量的时候多线程处理的优势会体现出来。