java concurrent 学习(1) – FutureTask原理

在多线程执行时,对于需要有返回值的场景,常常使用Callable和Future的方式来进行,常见的一种使用方式如下:

这里写图片描述
运行上面的代码,在控制台种等待三秒钟之后打印出结果。代码非常简单,但是有几个问题需要弄清楚:
1. 线程池是如何调用到Callable的call,结果是如何返回的。
2. Future.get方法是如何阻塞住当前线程的
3. 当Callable运行完是如何通知到阻塞在Future上面的线程的
下面我们开始分析源码解答上述问题(源码基于jdk1.8)。

首先从ExecutorService的submit(Callable)方法开始,此方法对应的实现类是ThreadPoolExecutor, 代码如下:
这里写图片描述
newTaskFor方法就是将callable转换成了FutureTask,FutrueTask也继承了RunnableFuture,获取到RunnableFuture后,调用了execute(ftask) ,我们继续向下跟代码
这里写图片描述
这里写图片描述
图片中的英文注释已经说的比较清楚,我这里在详细说一下,第一行:
int c = ctl.get(), 其中ctl是一个AtomicInteger,每当向线程池放入一个callable或者runnable,这个clt就+1,int c 获取到的就是当前线程池目前的任务数量,
情况1:如果c 小于当前线程池维护最小的工作线程(CorePoolSize)时,那么线程池就尝试开启一个工作线程来执行传入的callable,也就是下面执行的addWorker(command,true), 此方法会自动检查runState和workerCount,从而防止将添加的错误。
情况2:如果c大于等于CorePoolSize时,要判断当前线程池是否正在运行,其次判断callable是否可以加入队列(有可能加入不成功,例如使用了有界队列,队列可能满了),如果以上条件都满足,那么再次通过ctl获取一次数量,做二次检查,按照官方注释,写的是有可能在做完第一次检查后,当前线程池挂了,或者其他错误,那么此时需要做回滚,也就是if中的remove(command),从队列中删除刚才放入的callable,然后在调用reject拒绝此任务,如果二次检查也没有问题,再调用addWorker(null,false),我们发现参数和情况1不同,任务为null,意思是不要为当前任务分配工作线程(因为任务已经加入队列了,不需要立刻执行),false表示判断当前线程池工作线程的数量是否超过了maxPoolsize,有兴趣的同学,可以仔细研究下addWorker方法的源码。
情况3:如果调用addWorker返回false,说明当前线程池的工作线程数量超过了maxpoolSize了,那么新的任务需要拒绝(ps:上面描述的内容需要读者对线程池中,任务、工作线程、corePoolsize、maxPoolsize以及任务队列都熟悉才能理解)

根据上述分析,触发callable执行的代码在addWorker中,代码如下(addWorker的代码较长,我们看关键的部分):
这里写图片描述
这里写图片描述

红框部分就是触发执行callbale执行的地方,图片中第一行,firsttask就是我们上面创建的FutureTask,被Worker包装了一下,在获取Thread t,t的start方法就会触发FutureTask的run方法。我们再进入FutureTask的run方法,代码如下:
这里写图片描述
具体调用callable的方法就在result = c.call(), 至此上面的问题1已经解答了,callable是在何时调用的,执行结果赋值给FutureTask的result变量。
我们再看问题2,当调用FutureTask的get方法,如何阻塞调用线程的。在get方法中,如果判断当前直接没有结束,那么就调用awaitDone方法,代码如下:
这里写图片描述
awaitDone代码如下:
这里写图片描述
这里写图片描述
方法有些复杂,从第一行看起,deadline 经过计算 = 0,因为timed为false,向下看,之所以有for(;;)无线循环,因为下面用到了cas,需要自旋执行最终成功。进入循环体,首先判断当前调用get方法的线程是否被interrupt,如果被interrupt,那么直接返回不会阻塞,后面的几个if基本都是判断状态,如果当前FutureTask已经结束,那么就直接返回,不阻塞,然后初始化了WaitNode,当代码执行到红色框部分,说明当前futureTask没有执行完成,那么需要把上面初始化的WaitNode加入到队列,所谓的队列就是AQS(AbstractQueuedSynchronizer), AQS底层维护了一个双向链表,当多线程调用FutureTask的get方法时,多个线程就会被加入到这个链表,但是加入队列的过程需要锁的控制,这里的控制就是CAS,多个线程同时调用get方法时,当执行到这个红色框部分,每次只有一个线程可以加入链表成功,由于外层由for(;;),所以失败的线程会再次执行,然后又有一个线程执行成功,以此类推,所有的线程都会执行成功,加入队列。在向下看,由于我们调用的get方法,是没有时间限制的,所有timed=false,所以不会进入绿色框的代码块,对于加入队列成功的线程,只是当前线程对象加入队列,但是线程还在执行,此时queued=true,那么下次循环就会进入到蓝色框部分,LockSupport.park(),这个方法是阻塞,这个方法和wait()/notify()/notifyAll()很类似,但是wait之后,如果想唤醒指定的线程无法实现,只能notifyAll,不是很智能,LockSupport.park(thread)方法需要传入一个Thread,也就是阻塞的线程,在调用LockSupport.unPark(thread),传入的thread只要和park的thread相同,就可以唤醒指定的线程。

通过上面的描述,我们知道了,在get方法中,主要是通过park方法进行阻塞的,那是再哪里唤醒阻塞的线程的呢?其实上面也有提过,是再run方法中,因为run方法是调用callable.call的地方,callable执行成功后,会把值付给result变量,然后再调用set方法,在set方法中会调用LockSupport.unpark()代码如下:
这里写图片描述
set中会调用finishCompletion,代码如下:
这里写图片描述
从第一行开始分析,迭代waiters,waiters是上面说到的AQS对应的双向链表的头结点,需要把整个waiters对应的链表的线程全部唤醒。在循环体中,调用了SupportLock的unpark方法。
至此,FutureTask的大致过程已经分析完成,其中有些细节,笔者也没有了解特别深入,希望读者可以留言共同探讨。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
当在使用Java中的FutureTask类时,可能会遇到java.util.concurrent.FutureTask.report内存溢出的问题。 FutureTaskJava中Executor框架的一部分,用于表示一个异步计算任务。在执行计算任务时,可能会出现内存溢出的情况,这通常是由于以下原因引起的: 1. 任务计算量过大:如果计算任务需要处理大量的数据或执行复杂的操作,可能会导致内存使用过多,进而导致内存溢出。 2. 内存泄漏:如果在任务执行过程中有未正确释放的资源或对象引用,那么这些未释放的资源可能会一直保存在内存中,最终导致内存溢出。 解决这个问题的方法如下: 1. 优化任务计算:检查计算任务的实现代码,尝试减少计算量或改进计算逻辑,以降低内存使用量。可以通过合理使用数据结构、循环和递归等技巧来减少内存占用。 2. 调整内存配置:如果计算任务确实需要较大的内存空间,可以调整Java虚拟机的内存配置参数。可以增加-Xmx和-Xms等参数,以提供足够的内存空间给计算任务使用。 3. 检查内存泄漏:通过使用Java内存分析工具,如Eclipse Memory Analyzer等,来检查任务执行期间是否存在内存泄漏。如果发现泄漏问题,需要修复代码,确保资源和对象在不再使用时能够正确释放。 综上所述,当出现java.util.concurrent.FutureTask.report内存溢出的情况时,我们可以通过优化任务计算、调整内存配置和检查内存泄漏等方法来解决该问题。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值