Java中FutureTask详解

196 阅读3分钟

Future接口及其实现类FutureTask,代表异步计算的结果,都是JDK1.5新增的类。 Future中主要方法:

// 取消任务
boolean cancel(boolean mayInterruptIfRunning);
// 获取结果
V get() throws InterruptedException, ExecutionException;
// 获取结果(带超时)
V get(long timeout, TimeUnit unit)  throws InterruptedException, ExecutionException, TimeoutException;

FutureTask

FutureTask实现了RunnableFuture接口,间接实现了Runnable接口。因此,可以把FutureTask交给线程池执行。 image.png

  • volatile int state:关键属性,标识异步任务状态。对当前线程、异步线程都可见。 image.png
  • Callable callable:任务本身
  • Object outcome:结果,对Runnable任务返回null
  • Thread runner:执行任务的线程 image.png
  • WaitNode waiters:链表结构,记录了调用get()而阻塞的线程们。 image.png image.png

可见,FutureTask检测任务执行到什么状态,并持有最终结果,当主线程调用get()时,它会判断是否应当阻塞等待任务完成
当任务执行完成后,将waiters中记录的阻塞线程一一唤醒,拿到结果。

因此,FutureTask是对Runnable、Callable的增强,在线程间传递异步结果。

1 包装Runnable任务

Runnable类型任务无返回值,通常也不使用Future。但是也可以强行使用,如下:

ExecutorService pool = Executors.newSingleThreadExecutor();
Future<?> submit = pool.submit(() -> {
	// do something
});

其实,当向线程池submit()一个Runnable类型任务时,最终会被包装成callable接口的子类RunnableAdapter,结果固定就是null。 image.png image.png image.png image.png RunnableAdapter的run方法,只会返回预设的结果。 image.png

2 执行任务

当线程池执行任务someTask时,将调用FutureTask.run(),由它代为调用someTask.call方法。 image.png set()设置结果、更新状态,并唤醒等待结果而阻塞的线程。 image.png 当任务正常完成或异常时,都会调用finishCompletion()依次唤醒waiters中所有阻塞等待结果的线程。 image.png image.png

3 获取结果

任务执行完成时,主线程调get()会立即返回。否则,会将当前线程包装为WaitNode放入FutureTask的waiters属性,并调用LockSupport.park(this)阻塞当前线程。 image.png image.pngawaitDone(boolean timed, long nanos)中有如下代码:调用LockSupport.park(this) image.png 任务完成时,将结果设置到outcome属性。任务异常时抛出异常。 image.png

4 取消执行

调用cancel(boolean mayInterruptIfRunning)来取消任务。注意:mayInterruptIfRunning为true时,将中断执行线程。 image.png

5 使用FutureTask

5.1 结合Thread使用

把FutureTask作为任务,交给Thread或线程池执行。在主线程中能方便地获取异步结果、处理异常。

	FutureTask<Integer> task = new FutureTask<>(() -> {
		return 100;
	});

	new Thread(task, "calcu").start();
	Integer result = null;
	try {
		result = task.get();
	} catch (InterruptedException | ExecutionException e) {
		// 处理异常
	}
	// 使用result

5.2 实现线程同步

当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行,此时可以使用FutureTask。假设有多个线程执行若干任务,每个任务最多只能被执行一次。

如下场景:假设项目中请求外部接口需要先生成session,一旦生成可在一段时间有效。

  • executionTask保证了CREATE_SESSION任务只执行一次;
  • 当有多个线程并发执行CREATE_SESSION时,最多只有一个线程执行任务,其他线程阻塞等待结果。
@Slf4j(topic = "c.TestFutureTask")
public class TestFutureTask {
  private static final ConcurrentMap<String, Future<String>> taskCache = new ConcurrentHashMap<>();
  public static final String CREATE_SESSION = "session";

  // 仅执行一次
  private static String executionTask(final String taskName) throws ExecutionException, InterruptedException {
    Future<String> future = taskCache.get(taskName);
    if (future == null) {
      Callable<String> task = new Callable<String>() {
        public String call() throws InterruptedException {
          log.info("远程请求创建session");
          TimeUnit.SECONDS.sleep(3);
          return "aoeirkae";
        }
      };
      // 创建任务
      FutureTask<String> futureTask = new FutureTask<>(task);
      future = taskCache.putIfAbsent(taskName, futureTask);
      if (future == null) {
        future = futureTask;
        futureTask.run();
      }
    }

    try {
      return future.get();
    } catch (Exception e) {
      taskCache.remove(taskName, future);
      throw e;
    }
  }

  // 模拟并发请求CREATE_SESSION
  public static void main(String[] args) {
    ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);
    pool.prestartAllCoreThreads();
    Future<String> session1 = pool.submit(() -> {
      String session = executionTask(CREATE_SESSION);
      log.info("执行任务结束, session=" + session);
      return session;
    });
    Future<String> session2 = pool.submit(() -> {
      String session = executionTask(CREATE_SESSION);
      log.info("执行任务结束, session=" + session);
      return session;
    });
    Future<String> session3 = pool.submit(() -> {
      String session = executionTask(CREATE_SESSION);
      log.info("执行任务结束, session=" + session);
      return session;
    });
    pool.shutdown();
  }
}

image.png