前言

在并发编程当中我们最常见的需求就是启动一个线程执行一个函数去完成我们的需求,而在这种需求当中,我们常常需要函数有返回值。比如我们需要同一个非常大的数组当中数据的和,让每一个线程求某一个区间内部的和,最终将这些和加起来,那么每个线程都需要返回对应区间的和。而在Java当中给我们提供了这种机制,去实现这一个效果——FutureTask

FutureTask

在自己写FutureTask之前我们首先写一个例子来回顾一下FutureTask的编程步骤:

  • 写一个类实现Callable接口。
  1. @FunctionalInterface
  2. public interface Callable<V> {
  3. /**
  4. * Computes a result, or throws an exception if unable to do so.
  5. *
  6. * @return computed result
  7. * @throws Exception if unable to compute a result
  8. */
  9. V call() throws Exception;
  10. }

实现接口就实现call即可,可以看到这个函数是有返回值的,而FutureTask返回给我们的值就是这个函数的返回值。

  • new一个FutureTask对象,并且new一个第一步写的类,new FutureTask<>(callable实现类)
  • 最后将刚刚得到的FutureTask对象传入Thread类当中,然后启动线程即可new Thread(futureTask).start();
  • 然后我们可以调用FutureTaskget方法得到返回的结果futureTask.get();

假如有一个数组data,长度为100000,现在有10个线程,第i个线程求数组[i * 10000, (i + 1) * 10000)所有数据的和,然后将这十个线程的结果加起来。

  1. import java.lang.reflect.Array;
  2. import java.util.Arrays;
  3. import java.util.Random;
  4. import java.util.concurrent.Callable;
  5. import java.util.concurrent.ExecutionException;
  6. import java.util.concurrent.FutureTask;
  7. public class FutureTaskDemo {
  8. public static void main(String[] args) throws ExecutionException, InterruptedException {
  9. int[] data = new int[100000];
  10. Random random = new Random();
  11. for (int i = 0; i < 100000; i++) {
  12. data[i] = random.nextInt(10000);
  13. }
  14. @SuppressWarnings("unchecked")
  15. FutureTask<Integer>[] tasks = (FutureTask<Integer>[]) Array.newInstance(FutureTask.class, 10);
  16. // 设置10个 futuretask 任务计算数组当中数据的和
  17. for (int i = 0; i < 10; i++) {
  18. int idx = i;
  19. tasks[i] = new FutureTask<>(() -> {
  20. int sum = 0;
  21. for (int k = idx * 10000; k < (idx + 1) * 10000; k++) {
  22. sum += data[k];
  23. }
  24. return sum;
  25. });
  26. }
  27. // 开启线程执行 futureTask 任务
  28. for (FutureTask<Integer> futureTask : tasks) {
  29. new Thread(futureTask).start();
  30. }
  31. int threadSum = 0;
  32. for (FutureTask<Integer> futureTask : tasks) {
  33. threadSum += futureTask.get();
  34. }
  35. int sum = Arrays.stream(data).sum();
  36. System.out.println(sum == threadSum); // 结果始终为 true
  37. }
  38. }

可能你会对FutureTask的使用方式感觉困惑,或者不是很清楚,现在我们来仔细捋一下思路。

  1. 首先启动一个线程要么是继承自Thread类,然后重写Thread类的run方法,要么是给Thread类传递一个实现了Runnable的类对象,当然可以用匿名内部类实现。
  2. 既然我们的FutureTask对象可以传递给Thread类,说明FutureTask肯定是实现了Runnable接口,我们现在来看一下FutureTask的继承体系。

​ 可以发现的是FutureTask确实实现了Runnable接口,同时还实现了Future接口,这个Future接口主要提供了后面我们使用FutureTask的一系列函数比如get

  1. 看到这里你应该能够大致想到在FutureTask中的run方法会调用Callable当中实现的call方法,然后将结果保存下来,当调用get方法的时候再将这个结果返回。

自己实现FutureTask

工具准备

经过上文的分析你可能已经大致了解了FutureTask的大致执行过程了,但是需要注意的是,如果你执行FutureTaskget方法是可能阻塞的,因为可能Callablecall方法还没有执行完成。因此在get方法当中就需要有阻塞线程的代码,但是当call方法执行完成之后需要将这些线程都唤醒。

在本篇文章当中使用锁ReentrantLock和条件变量Condition进行线程的阻塞和唤醒,在我们自己动手实现FutureTask之前,我们先熟悉一下上面两种工具的使用方法。

  • ReentrantLock主要有两个方法:

    • lock对临界区代码块进行加锁。
    • unlock对临界区代码进行解锁。
  • Condition主要有三个方法:
    • await阻塞调用这个方法的线程,等待其他线程唤醒。
    • signal唤醒一个被await方法阻塞的线程。
    • signalAll唤醒所有被await方法阻塞的线程。
  1. import java.util.concurrent.TimeUnit;
  2. import java.util.concurrent.locks.Condition;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. public class LockDemo {
  5. private ReentrantLock lock;
  6. private Condition condition;
  7. LockDemo() {
  8. lock = new ReentrantLock();
  9. condition = lock.newCondition();
  10. }
  11. public void blocking() {
  12. lock.lock();
  13. try {
  14. System.out.println(Thread.currentThread() + " 准备等待被其他线程唤醒");
  15. condition.await();
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }finally {
  19. lock.unlock();
  20. }
  21. }
  22. public void inform() throws InterruptedException {
  23. // 先休眠两秒 等他其他线程先阻塞
  24. TimeUnit.SECONDS.sleep(2);
  25. lock.lock();
  26. try {
  27. System.out.println(Thread.currentThread() + " 准备唤醒其他线程");
  28. condition.signal(); // 唤醒一个被 await 方法阻塞的线程
  29. // condition.signalAll(); // 唤醒所有被 await 方法阻塞的线程
  30. }finally {
  31. lock.unlock();
  32. }
  33. }
  34. public static void main(String[] args) {
  35. LockDemo lockDemo = new LockDemo();
  36. Thread thread = new Thread(() -> {
  37. lockDemo.blocking(); // 执行阻塞线程的代码
  38. }, "Blocking-Thread");
  39. Thread thread1 = new Thread(() -> {
  40. try {
  41. lockDemo.inform(); // 执行唤醒线程的代码
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. }, "Inform-Thread");
  46. thread.start();
  47. thread1.start();
  48. }
  49. }

上面的代码的输出:

  1. Thread[Blocking-Thread,5,main] 准备等待被其他线程唤醒
  2. Thread[Inform-Thread,5,main] 准备唤醒其他线程

FutureTask设计与实现

在前文当中我们已经谈到了FutureTask的实现原理,主要有以下几点:

  • 构造函数需要传入一个实现了Callable接口的类对象,这个将会在FutureTaskrun方法执行,然后得到函数的返回值,并且将返回值存储起来。
  • 当线程调用get方法的时候,如果这个时候Callable当中的call已经执行完成,直接返回call函数返回的结果就行,如果call函数还没有执行完成,那么就需要将调用get方法的线程挂起,这里我们可以使用condition.await()将线程挂起。
  • call函数执行完成之后,需要将之前被get方法挂起的线程唤醒继续执行,这里使用condition.signalAll()将所有挂起的线程唤醒。
  • 因为是我们自己实现FutureTask,功能不会那么齐全,只需要能够满足我们的主要需求即可,主要是帮助大家了解FutureTask原理。

实现代码如下(分析都在注释当中):

  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.Condition;
  4. import java.util.concurrent.locks.ReentrantLock;
  5. // 这里需要实现 Runnable 接口,因为需要将这个对象放入 Thread 类当中
  6. // 而 Thread 要求传入的对象实现了 Runnable 接口
  7. public class MyFutureTask<V> implements Runnable {
  8. private final Callable<V> callable;
  9. private Object returnVal; // 这个表示我们最终的返回值
  10. private final ReentrantLock lock;
  11. private final Condition condition;
  12. public MyFutureTask(Callable<V> callable) {
  13. // 将传入的 callable 对象存储起来 方便在后面的 run 方法当中调用
  14. this.callable = callable;
  15. lock = new ReentrantLock();
  16. condition = lock.newCondition();
  17. }
  18. @SuppressWarnings("unchecked")
  19. public V get(long timeout, TimeUnit unit) {
  20. if (returnVal != null) // 如果符合条件 说明 call 函数已经执行完成 返回值已经不为 null 了
  21. return (V) returnVal; // 直接将结果返回即可 这样不用竞争锁资源 提高程序执行效率
  22. lock.lock();
  23. try {
  24. // 这里需要进行二次判断 (双重检查)
  25. // 因为如果一个线程在第一次判断 returnVal 为空
  26. // 然后这个时候它可能因为获取锁而被挂起
  27. // 而在被挂起的这段时间,call 可能已经执行完成
  28. // 如果这个时候不进行判断直接执行 await方法
  29. // 那后面这个线程将无法被唤醒
  30. if (returnVal == null)
  31. condition.await(timeout, unit);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. } finally {
  35. lock.unlock();
  36. }
  37. return (V) returnVal;
  38. }
  39. @SuppressWarnings("unchecked")
  40. public V get() {
  41. if (returnVal != null)
  42. return (V) returnVal;
  43. lock.lock();
  44. try {
  45. // 同样的需要进行双重检查
  46. if (returnVal == null)
  47. condition.await();
  48. } catch (InterruptedException e) {
  49. e.printStackTrace();
  50. } finally {
  51. lock.unlock();
  52. }
  53. return (V) returnVal;
  54. }
  55. @Override
  56. public void run() {
  57. if (returnVal != null)
  58. return;
  59. try {
  60. // 在 Runnable 的 run 方法当中
  61. // 执行 Callable 方法的 call 得到返回结果
  62. returnVal = callable.call();
  63. } catch (Exception e) {
  64. e.printStackTrace();
  65. }
  66. lock.lock();
  67. try {
  68. // 因为已经得到了结果
  69. // 因此需要将所有被 await 方法阻塞的线程唤醒
  70. // 让他们从 get 方法返回
  71. condition.signalAll();
  72. }finally {
  73. lock.unlock();
  74. }
  75. }
  76. // 下面是测试代码
  77. public static void main(String[] args) {
  78. MyFutureTask<Integer> ft = new MyFutureTask<>(() -> {
  79. TimeUnit.SECONDS.sleep(2);
  80. return 101;
  81. });
  82. Thread thread = new Thread(ft);
  83. thread.start();
  84. System.out.println(ft.get(100, TimeUnit.MILLISECONDS)); // 输出为 null
  85. System.out.println(ft.get()); // 输出为 101
  86. }
  87. }

我们现在用我们自己写的MyFutureTask去实现在前文当中数组求和的例子:

  1. public static void main(String[] args) throws ExecutionException, InterruptedException {
  2. int[] data = new int[100000];
  3. Random random = new Random();
  4. for (int i = 0; i < 100000; i++) {
  5. data[i] = random.nextInt(10000);
  6. }
  7. @SuppressWarnings("unchecked")
  8. MyFutureTask<Integer>[] tasks = (MyFutureTask<Integer>[]) Array.newInstance(MyFutureTask.class, 10);
  9. for (int i = 0; i < 10; i++) {
  10. int idx = i;
  11. tasks[i] = new MyFutureTask<>(() -> {
  12. int sum = 0;
  13. for (int k = idx * 10000; k < (idx + 1) * 10000; k++) {
  14. sum += data[k];
  15. }
  16. return sum;
  17. });
  18. }
  19. for (MyFutureTask<Integer> MyFutureTask : tasks) {
  20. new Thread(MyFutureTask).start();
  21. }
  22. int threadSum = 0;
  23. for (MyFutureTask<Integer> MyFutureTask : tasks) {
  24. threadSum += MyFutureTask.get();
  25. }
  26. int sum = Arrays.stream(data).sum();
  27. System.out.println(sum == threadSum); // 输出结果为 true
  28. }

总结

在本篇文章当中主要给大家介绍了FutureTask的内部原理,并且我们自己通过使用ReentrantLockCondition实现了我们自己的FutureTask,本篇文章的主要内容如下:

  • FutureTask的内部原理:

    • FutureTask首先会继承Runnable接口,这样就可以将FutureTask的对象直接放入Thread类当中,作为构造函数的参数。
    • 我们在使用FutureTask的时候需要传入一个Callable实现类的对象,在函数call当中实现我们需要执行的函数,执行完成之后,将call函数的返回值保存下来,当有线程调用get方法时候将保存的返回值返回。
  • 我们使用条件变量进行对线程的阻塞和唤醒。
    • 当有线程调用get方法时,如果call已经执行完成,那么可以直接将结果返回,否则需要使用条件变量将线程挂起。
    • call函数执行完成的时候,需要使用条件变量将所有阻塞在get方法的线程唤醒。
  • 双重检查:
    • 我们在get方法当中首先判断returnVal是否为空,如果不为空直接将结果返回,这就可以不用去竞争锁资源了,可以提高程序执行的效率。
    • 但是我们在使用锁保护的临界区还需要进行判断,判断returnVal是否为空,因为如果一个线程在第一次判断 returnVal 为空,然后这个时候它可能因为获取锁而被挂起, 而在被挂起的这段时间,call 可能已经执行完成,如果这个时候不进行判断直接执行 await方法,那后面这个线程将无法被唤醒,因为在call函数执行完成之后调用了condition.signalAll(),如果线程在这之后执行await方法,那么将来再没有线程去将这些线程唤醒。

更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

60行从零开始自己动手写FutureTask是什么体验?的更多相关文章

  1. 60行自己动手写LockSupport是什么体验?

    60行自己动手写LockSupport是什么体验? 前言 在JDK当中给我们提供的各种并发工具当中,比如ReentrantLock等等工具的内部实现,经常会使用到一个工具,这个工具就是LockSupp ...

  2. 60行代码:Javascript 写的俄罗斯方块游戏

    哈哈这个实在是有点意思 备受打击当初用java各种类写的都要几百行啦 先看效果图: 游戏结束图: javascript实现源码: [javascript] view plaincopyprint? & ...

  3. 60行以内写mvc

    标题党.几天前看到一个30行写mvc的文章,东施效颦,也动手写了个60行的,功能上略微扩充一些,记录下来,后面有时间可以继续优化. mvc其实是一个观察者模式.view来监听model,所以当mode ...

  4. 死磕 java线程系列之自己动手写一个线程池(续)

    (手机横屏看源码更方便) 问题 (1)自己动手写的线程池如何支持带返回值的任务呢? (2)如果任务执行的过程中抛出异常了该怎么处理呢? 简介 上一章我们自己动手写了一个线程池,但是它是不支持带返回值的 ...

  5. 教你看懂网上流传的60行JavaScript代码俄罗斯方块游戏

    早就听说网上有人仅仅用60行JavaScript代码写出了一个俄罗斯方块游戏,最近看了看,今天在这篇文章里面我把我做的分析整理一下(主要是以注释的形式). 我用C写一个功能基本齐全的俄罗斯方块的话,大 ...

  6. 【转】自己动手写SC语言编译器

    自序 编译原理与技术的一整套理论在整个计算机科学领域占有相当重要的地位,学习它对程序设计人员有很大的帮助.我们考究历史会发现那些人人称颂的程序设 计大师都是编译领域的高手,像写出BASIC语言的BIL ...

  7. 自己动手写 ASP.NET MVC 分页 part1

    学习编程也有一年半载了,从来没有自己动手写过东西,都是利用搜索软件找代码,最近偶发感慨,难道真的继续做码农??? 突发奇想是不是该自己动手写点东西,可是算法.逻辑思维都太弱了,只能copy网上的代码, ...

  8. 自己动手写把”锁”---LockSupport介绍

    本篇是<自己动手写把"锁">系列技术铺垫的最后一个知识点.本篇主要讲解LockSupport工具类,它用来实现线程的挂起和唤醒. LockSupport是Java6引入 ...

  9. 自己动手写把”锁”---LockSupport深入浅出

    本篇是<自己动手写把"锁">系列技术铺垫的最后一个知识点.本篇主要讲解LockSupport工具类,它用来实现线程的挂起和唤醒. LockSupport是Java6引入 ...

随机推荐

  1. Typora 开始收费,改用好玩的MarkText

    收费-- 可以考虑使用:MarkText 简述MarkText MarkText 这个工具侧重于"命令",导航栏都被收起来了.有些小伙伴感觉反而不好用,其实不然,是未了解该工具的强 ...

  2. MAC M1安装多个JDK版本及动态切换

    JDK版本下载 下载地址:https://www.azul.com/downloads/?package=jdk 筛选一下macOS的ARM 64-bit架构的JDK版本,下载对应版本即可.最好直接下 ...

  3. mongoDB 命令大全

    每日一句 There should be a better way to start a day than waking up every morning. 应该有更好的方式开始新一天, 而不是千篇一 ...

  4. 企业应用架构研究系列二十六:信号量SemaphoreSlim与Semaphore

    在进行多线程程序的开发和设计的过程中,不可避免的需要引入semaphore信号量这个组件,这是.net框架提供的一个对多线程计数互斥的方案,就是允许指定的线程个数访问特定的资源而增加的 一个" ...

  5. 『忘了再学』Shell基础 — 24、Shell正则表达式的使用

    目录 1.正则表达式说明 2.基础正则表达式 3.练习 (1)准备工作 (2)*练习 (3).练习 (4)^和$练习 (5)[]练习 (6)[^]练习 (7)\{n\}练习 (8)\{n,\}练习 ( ...

  6. 计算机网络 - HTTP和HTTPS的区别

    计算机网络 - HTTP和HTTPS的区别 http所有传输的内容都是明文,并且客户端和服务器端都无法验证对方的身份. https具有安全性的ssl加密传输协议,加密采用对称加密. https协议需要 ...

  7. 运筹学笔记12 大M法

    引入M,其中M是一个充分大的正数.由此,目标函数也改变为zM. 如此构造的线性规划问题我们记作LPM,称之为辅助线性规划问题,也即在原来的线性规划问题的基础上,改造了其等式约束条件,然后有对目标函数施 ...

  8. Java实用类-Enum(枚举)

    1. 历史 ​ 在 JDK 1.5 之前没有枚举类型,那时候一般用接口常量来替代(例如,public static final String male ).JKD1.5之后使用 Java 枚举类型 e ...

  9. 如何写出同事看不懂的Java代码?

    原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 哈喽大家好啊,我是没更新就是在家忙着带娃的Hydra. 前几天,正巧赶上组里代码review,一下午下来,感觉整个人都血压拉满了.五花八门的代码 ...

  10. RocketMQ消息的顺序与重复

    1.如何保证消息的顺序 原因:生产者将消息发给topic,topic分发给不同的队列再给多个消费者并发消费,难以保证顺序. 方案:topic和队列之间加入MessageQueueSelector.将一 ...