Post

Java Fork Join

Fork/Join计算框架

随着多核计算机成为主流,各种编程语言也在不停演进以最大化利用多核计算机的计算能力。Java最开始以Thread对象作为核心,构建最初的多线程模型。从JDK1.5加入的J.U.C,通过对线程和线程池的增强,极大便利了在Java平台下开发多线程应用,在J.U.C中,我们一般通过定义线程的执行器例如ExecutorService然后向执行器中提交线程任务,ExecutorService会在内部利用多线程机制,对提交的线程任务进行调度和管理,并对外提供API控制,正常每一个线程任务都会返回一个Future,我们只需要通过对这个对象进行get就可以获取对应的执行结果。

这边补充一下线程/进程/并发/并行的概念理解:

线程和进程都是程序代码执行的上下文环境。一般可以认为,进程是计算机分配资源的最小单位;而进程是独立运行和调度的最小单位;例如一个Tomcat程序就是一个Java进程,而Tomcat内部,则会创建多个线程,这些线程公用这个进程的资源,也就是进程是线程的宿主。在Linux平台下,线程是轻量级的进程,多线程实际上是几个共享内存空间的进程。

并发强调是在一个时间段内,多个任务可以一起执行,如果对于单核处理器来说,最典型的就是进行时间分片处理,每一个任务分别获取一小段处理时间,并在CPU上交替进行处理,显然一个CPU核心也可以进行并发计算,并且当任务数量多余CPU核心数量的时候,就需要进行任务的切换。

并行强调的是同一个时刻,同时发生,也就是在一个时间点,多个任务可以同时发生。显然要做到并行,必须要有多个CPU核心。

另一类问题是一个大的问题,可以拆分成多个小的任务分别进行计算,再合并后得到,也就是Map-Reduce,例如计算1到10000的数字的总和,使用上面介绍的方法,我们可以很容易讲问题分解为计算1-5000和5000-10000的数字总和,并将两个任务的和相加之后即为原问题的答案。

在JDK1.7加入的Fork/Join模型,也是为了能够最大化利用多核处理器提升整体应用的性能。fork/join框架将任务分布到线程池中的不同线程,这个和J.U.C中的其他并发模型并没有区别,fork/join的特殊之处是其使用了work-strealing算法,在一些线程完成任务之后,可以从其他正在运行线程的未完成任务列表中获取任务。

如果我们基于旧的编程API进行DIY实现,我们可能会使用一个TaskQueue存储每一个线程对应的任务,在线程完成自身的TaskQueue之后,再从其他的TaskQueue中获取任务加入到自己的TaskQueue中,重复进行计算。这样实现似乎是很简单的,但是会有哪些问题?

  1. 任务的划分问题,如何将一个大任务分割成为小的任务。
  2. 数据竞争的问题,在进行work-stealing的时候,如何保证任务有且仅有被执行一次。
  3. 合并问题,多个线程执行完成之后的结果合并问题。
  4. 异常处理,如果工作线程出现异常,应该怎么进行处理。

下面通过介绍JDK1.7中引入的Fork/Join框架的使用,最后研究一下其如何解决我们自行设计会遇到的四个问题。

This post is licensed under CC BY 4.0 by the author.