Android中Callable、Future、FutureTask的概念以及几种线程池的使用

2,395 阅读7分钟

我正在参加「掘金·启航计划」

线程池必备知识

在开始介绍线程池之前,先来介绍下CallableFuture的概念,众所周知,Android中实现多线程的方式有两种,实现Runnable接口或者继承一个Thread,但是这两种方式都有一个缺点:在任务执行完成之后没有返回结果,所以在Java 1.5之后,出现了CallableFuture,通过他们构建的线程,可以在线程执行完成之后得到返回结果。

先来对比Runnable 和Callable

Runnable

public interface Runnable {  
    public abstract void run();  
}  

Callable

public interface Callable<V> {
  V call() throws Exception;
 }

Runnable 接口中的run()方法的返回值是void,所以Runnable无返回值;而Callable接口中的call()方法的返回值是V,也可以抛出异常,所以Callable是可以有返回值的。接着来看下Future

Future

public interface Future<V> {
  boolean cancel(boolean mayInterruptIfRunning);
  
  boolean isCancelled();

  boolean isDone();

  V get() throws InterruptedException, ExecutionException;

  V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
 }

Future<V>用来获取异步计算的结果,即是获取Callable<V>任务的结果

Future备注
cancel(boolean)尝试取消异步任务的执行。如果任务已经执行完成、已经被取消、因为某种原因不能被取消,则返回false;如果任务正在执行,并且mayInterruptIfRunning为true,那么会调用interrupt()尝试打断任务。该方法返回结果后,isDone()总会返回true
isCancelled()如果在任务完成前被取消,返回true
isDone()如果任务完成则返回true。任务完成包括正常结束、任务被取消、任务发生异常,都返回true
get()获取异步任务执行结果,如果没有返回,则阻塞等待
get(long timeout, TimeUnit unit)在给定的时间内等待获取异步任务结果,如果超时还未获取到结果,则会抛出TimeoutException

Future<V>只是一个接口,还需要看Future的具体实现类:

future.png 以FutureTask为例,具体分析下FutureTask:

public class FutureTask<V> implements RunnableFuture<V> {
  ................其他.....................
  }

咦?FutureTask并没有实现Future接口,而是实现了一个叫RunnableFuture的接口,这货从哪里蹦出来的?我们点进去看一下:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

吆西!原来这货不仅继承了Future接口,还继承了Runnable接口(PS:接口可以多继承),那么我们的FutureTask也就实现了RunnableFuture接口

FutureTask备注
boolean isCancelled()同Future
boolean isDone()同Future
boolean cancel(boolean mayInterruptIfRunning)同Future
V get()同Future
V get(long timeout, TimeUnit unit)同Future
void run()如果这个任务没有被取消,将直接在当前线程内执行任务

FutureTask有两个构造方法:

public FutureTask(Callable<V> callable)
public FutureTask(Runnable runnable, V result)

我们看到FutureTask初始化时不仅可以传入Callable,还可以传入一个Runnable和一个"返回结果result",这里为什么返回结果要加引号呢,来看下源码就知道了:

public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

上面FutureTask()的初始化构造参数中调用了Executors.callable()

public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    return new RunnableAdapter<T>(task, result);
}

private static final class RunnableAdapter<T> implements Callable<T> {
    private final Runnable task;
    private final T result;
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        task.run();
         //把传入的值直接返回
        return result;
    }
}

通过源码我们看到,在最后的call()方法中,直接把传入的值返回了,所以FutureTask(Runnable runnable, V result)得到的只是预设的结果,Runnable并不会得到执行的结果。如果不需要预设的返回结果,可以像下面这样初始化:

Future<?> f = new FutureTask<Void>(runnable, null)

FutureTask的使用:

 //初始化一个线程池
 ExecutorService executor = = Executors.newSingleThreadExecutor();
 //new 一个Callable并传入FutureTask
 FutureTask<String> future =new FutureTask<>(new Callable<String>() {
     public String call() {
        //do something
       return result;
   }});
 executor.execute(future);
 //在异步任务执行期间可以做一些其他的事情
 displayOtherThings();
 //通过future.get()得到异步任务执行结果
 String result=future.get();

ExecutorService

ExecutorService继承自Executor接口,并提供了管理线程以及创建可以追踪一个或多个异步任务的进展的Future的方法。

public interface ExecutorService extends Executor {
  
  //无法提交新任务,但是已经提交的任务将继续执行,当执行完成后关闭线程池
  void shutdown();
  //尝试停止所有正在执行的任务,暂停等待任务的处理,并返回等待执行的任务列表。
  List<Runnable> shutdownNow();
  //如果线程池已经关闭则返回true
  boolean isShutdown();
  //如果所有任务都在线程池关闭后完成,则返回true。注意,除非首先调用 shutdown 或 shutdownNow,否则 isTerminated 永不为 true。
  boolean isTerminated();
  //阻塞等待,直到所有任务在关闭请求后完成执行,或者超时发生,或者当前线程被中断
  // 如果此执行程序终止,则返回 true;如果终止前超时了,则返回 false 
  boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
 
  <T> Future<T> submit(Callable<T> task);
 
  <T> Future<T> submit(Runnable task, T result);
  //提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功 完成时将会返回 null
  Future<?> submit(Runnable task);
 
  <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
 
  //执行给定的任务,当所有任务完成或超时期满时(无论哪个首先发生),返回保持任务状态和结果的 Future 列表。
  //返回列表的所有元素的 Future.isDone() 为 true。一旦返回后,即取消尚未完成的任务。
  //注意,可以正常地或通过抛出异常来终止已完成 任务。如果此操作正在进行时修改了给定的
  // collection,则此方法的结果是不确定的。
  <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, 
             long timeout, TimeUnit unit)
        throws InterruptedException;
 
  <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
  //执行给定的任务,如果在给定的超时期满前某个任务已成功完成(也就是未抛出异常),则返回其结果。
  //一旦正常或异常返回后,则取消尚未完成的任务。如果此操作正在进行时修改了给定的 collection,
  //则此方法的结果是不确定的。
  <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
 }

线程池的使用

使用线程池的好处:

  • 当执行大量异步任务时,线程池能提供更好的性能体验,因为线程池能减少每个任务的调用开销,重用存在的线程,减少对象创建、消亡的开销
  • 还可以提供绑定和管理资源 (包括执行任务时使用的线程) 的方法。有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
  • 提供定时执行、定期执行、单线程、并发数控制等功能。

Android中提供了四种线程池,四种线程池内部实现都是直接或者间接用ThreadPoolExecutor

Executors.newCachedThreadPool()

只有Integer.MAX_VALUE个非核心线程,当有任务来时,如果线程池中的线程都处于活动状态,那么会新建线程来执行,否则就会利用空闲线程去执行,空闲线程都会有一个超时机制,超过60秒的空闲线程会被回收。任务队列为空集合,所以所有任务都会被立即执行,CachedThreadPool适合执行大量的耗时较少的操作。

效果图: CachedThreadPool.gif

因为非核心线程数有无限个,所以不管有多少任务都可以并行执行,可以看到上述5个任务一起执行,核心代码:

//初始化线程池
ExecutorService threadPool= Executors.newCachedThreadPool();
//初始化Callable
Callable<Integer> callable = new Callable<Integer>() {
    int num = 0;
    @Override
    public Integer call() throws Exception {
        while (num < 100) {
            num++;
            sendMsg(num, what);
            Thread.sleep(50);
        }
        return 100;
    }
 };
//执行线程任务
threadPool.submit(callable);

Executors.newFixedThreadPool(int nThreads)

只有核心线程并且不会被回收,任务队列没有大小限制。

效果图: FixedThreadPool.gif 因为核心线程数参数我们传入的是4,可所以看到先执行其中的4个任务,等待有任务执行完成后接着去执行第5个任务,核心代码:

//初始化线程池
ExecutorService threadPool= Executors.newFixedThreadPool(4);
//初始化Callable
Callable<Integer> callable = new Callable<Integer>() {
    int num = 0;
    @Override
    public Integer call() throws Exception {
        while (num < 100) {
            num++;
            sendMsg(num, what);
            Thread.sleep(50);
        }
        return 100;
    }
 };
//执行线程任务
threadPool.submit(callable);

Executors.newSingleThreadExecutor()

内部只有一个核心线程,所有任务按顺序执行 统一所有任务到一个线程中,使得这些任务不用处理线程同步问题。

效果图: SingleThreadExecutor.gif

可以看到每次只能执行一个任务,也就是所有任务都是串行执行的,核心代码:

//初始化线程池
ExecutorService threadPool= Executors.newSingleThreadExecutor();
//初始化Callable
Callable<Integer> callable = new Callable<Integer>() {
    int num = 0;
    @Override
    public Integer call() throws Exception {
        while (num < 100) {
            num++;
            sendMsg(num, what);
            Thread.sleep(50);
        }
        return 100;
    }
 };
//执行线程任务
threadPool.submit(callable);

Executors.newScheduledThreadPool(int corePoolSize)

核心线程是固定的,非核心线程是不固定的,非核心线程闲置时会被立即回收,主要用于执行定时任务和具有周期性的重复任务。

效果图: Schedule.gif ScheduledExecutorService传入的核心线程数是4,并且是在延迟2秒之后执行的,核心代码:

//初始化线程池,核心线程数为4
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(4);
//初始化Callable
Callable<Integer> callable = new Callable<Integer>() {
    int num = 0;
    @Override
    public Integer call() throws Exception {
        while (num < 100) {
            num++;
            sendMsg(num, what);
            Thread.sleep(50);
        }
        return 100;
    }
};
//延迟2秒后执行
scheduledPool.schedule(callable, 2, TimeUnit.SECONDS);

除上述延迟执行的方法外,ScheduledExecutorService中还有下列方法:

public interface ScheduledExecutorService extends ExecutorService {
    //延迟delay(TimeUnit 为单位)之后执行Runnable 
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);
    //延迟delay(TimeUnit 为单位)之后执行Callable
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);
    //延迟initialDelay之后每period时间执行一次Callable,循环执行
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
     //第一次延迟initialDelay之后执行,之后在每次完成后延迟delay时间执行下一次操作
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);
 }

示例代码

本文例子中完整代码已上传github:Android线程池的使用