随着多核处理器的普及以及众核处理器设计技术的不断发展,基于多核平台的并行软件开发将成为未来软件开发的主流,越来越多的程序将运行在多核平台上,如何提高程序的性能是摆在程序设计者面前的一个难题。
为了满足多核时代并发程序设计的要求,Java从JDK1.7版本开始引入了Fork/Join框架编程模式,该框架是适用于多核处理器上并行编程的轻量级并行框架,可以更好的利用多核处理器的处理能力,从而更好的提高程序的性能。
Fork/Join框架的主要思想是“分治方法”,即分而治之, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
正如Fork/Join框架的名称所展示的,Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。比如计算1+2+…+10000,可以分割成10个子任务,每个子任务分别对1000个数进行求和,最终汇总这10个子任务的结果。这两个操作有些类似于MapReduce中的map/reduce。
Fork/Join框架使用了工作窃取算法,已经完成自身任务的线程可以从其他工作繁忙的线程中窃取任务来执行,从而保证了线程执行过程中的负载均衡。
为什么需要使用工作窃取算法呢?
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
Fork/Join框架需要对任务进行分解和合并操作,在分解之前,首先查看问题的规模是否超过了预设的门槛值,在任务不大的情况下,采用串行的解决方式。 当问题的规模超过了预设的门槛值时,采用Fork/Join框架求解。
if(满足某个条件){
//使用串行模式解决或者选择其他算法
}else{
//1)将任务Task进行分解,分解为若干个小任务,Task1,Task2...
//2)将任务Task1,Task2...提交给线程池执行
//3)如果任务有返回结果,手机结果
}
伪代码:
public class Hint {
MyClass extends RecursiveTask{
@override
compute{
if(满足某个条件)
{do my work}
else
{
invokeAll(myClass1 = new MyClass(),myClass2 = new MyClass())
result = myClass1.join()+myClass2.join();
}
}
}
主线程里:
1) MyClass myClass = new MyClass();
2) pool = new ForkJoinPool();
3) pool.invoke(myClass);
4) result = myClassInstance.join();
}
Fork/Join框架的编程模式主要通过ForkJoinPool和ForkJoinTask两个类来完成。
ForkJoinPool:实现ExecutorService接口和工作窃取算法。它管理工作线程和提供关于任务的状态和它们执行的信息。
ForkJoinTask: 它是将在ForkJoinPool中执行的任务的基类。它提供在任务中执行fork()和join()操作的机制,并且这两个方法控制任务的状态。通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:
RecursiveTask:用于有返回结果的任务。
RecursiveAction:用于没有返回结果的任务。
类ForkJoinPool是 Fork/Join框架的核心,也是 Fork/Join框架执行的入口点。它实现ExecutorService接口和工作窃取算法。类ForkJoinPool的任务是负责管理线程,并提供线程执行状态和任务处理的相关信息。
ForkJoinPool提供了三类方法来调度子任务:
execute 系列:异步执行指定的任务,没有返回值。
invoke 和 invokeAll:执行指定的任务,等待完成,有返回值 。
submit 系列:异步执行指定的任务并立即返回一个 Future 对象。
类ForkJoinTask是将在ForkJoinPool中执行的任务的基类。它提供在任务中执行fork()和join()操作的机制,并且这两个方法控制任务的状态。通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:
RecursiveTask:用于有返回结果的任务。
RecursiveAction:用于没有返回结果的任务。
从类RecursiveAction继承的子类方法一般没有返回值。继承后的新类要重写 该类的compute方法。
@Override
protected void compute() {
}
常用方法包括:
1)isDone():用于判断任务是否完成。
2)cancel(boolean mayInterruptIfRunning):用于取消一个任务的执行。
从类RecursiveTask继承时通常要指明一个特定的数据类型,例如:
public class SumDirsFiles extends RecursiveTask {}
类SumDirsFiles继承自RecursiveTask,并对整数类型进行操作。
从类RecursiveTask继承的子类需要重写protected
方法,该方法有返回值,通过泛型T指明返回值的类型。
获取返回值有如下两种方法:
加粗样式1)方法join:该方法与Thread.join的方法不同,用于获取执行的结果。
2)方法get:当任务结束后返回任务的计算结果。
遍历指定目录(含子目录)统计文件个数,有返回值。从类RecursiveTask继承创建任务。同步执行。
public class SumDirsFiles extends RecursiveTask {
private File path;
public SumDirsFiles(File path) {
this.path = path;
}
@Override
protected Integer compute() {
int count = 0;
int dirCount = 0;
List subTasks = new ArrayList<>();
File[] files = path.listFiles();
if (files != null){
for (File file : files) {
if (file.isDirectory()) {
// 对每个子目录都新建一个子任务。
subTasks.add(new SumDirsFiles(file));
dirCount++;//统计目录个数
} else {
count++;// 遇到文件,文件个数加1。
}
}
System.out.println("目录:" + path.getAbsolutePath()
+"包含目录个数:"+dirCount+",文件个数:"+count);
if (!subTasks.isEmpty()) {
// 在当前的 ForkJoinPool 上调度所有的子任务。
for (SumDirsFiles subTask : invokeAll(subTasks)) {
count = count+subTask.join();
}
}
}
return count;
}
public static void main(String [] args){
try {
// 用一个 ForkJoinPool 实例调度“总任务”
ForkJoinPool pool = new ForkJoinPool();
//new一个ForkJoinTask的实例
SumDirsFiles task = new SumDirsFiles(new File("E:/"));
pool.invoke(task);//提交给ForkJoinPool执行Task
System.out.println("Task is Running......");
System.out.println("File counts ="+task.join());
System.out.println("Task end");
} catch (Exception e) {
e.printStackTrace();
}
}
}
遍历指定目录(含子目录)找寻指定类型文件,无返回值。从类RecursiveAction继承创建任务。异步执行。
public class FindDirsFiles extends RecursiveAction {
private File path;
public FindDirsFiles(File path) {
this.path = path;
}
@Override
protected void compute() {
List subTasks = new ArrayList<>();
File[] files = path.listFiles();
if (files!=null){
for (File file : files) {
if (file.isDirectory()) {
// 对每个子目录都新建一个子任务。
subTasks.add(new FindDirsFiles(file));
} else {
// 遇到文件,检查。
if (file.getAbsolutePath().endsWith("txt")){
System.out.println("文件:" + file.getAbsolutePath());
}
}
}
if (!subTasks.isEmpty()) {
// 在当前的 ForkJoinPool 上调度所有的子任务。
for (FindDirsFiles subTask : invokeAll(subTasks)) {
subTask.join();
}
}
}
}
public static void main(String [] args){
try {
// 用一个 ForkJoinPool 实例调度总任务
ForkJoinPool pool = new ForkJoinPool();
FindDirsFiles task = new FindDirsFiles(new File("E:/"));
pool.execute(task);
System.out.println("Task is Running......");
//异步任务
Thread.sleep(5);
int otherWork = 0;
for(int i=0;i<100;i++){
otherWork = otherWork+i;
}
System.out.println("Main Thread done sth......,otherWork="+otherWork);
task.join();//阻塞的方法
System.out.println("Task end");
} catch (Exception e) {
e.printStackTrace();
}
}
}
注:
同步交互:指发送一个请求,需要等待返回,然后才能够发送下一个请求,有个等待过程;
异步交互:指发送一个请求,不需要等待返回,随时可以再发送下一个请求,即不需要等待。 区别:一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式。
ForkJoinTask在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过ForkJoinTask的getException方法获取异常。
if(task.isCompletedAbnormally())
{
System.out.println(task.getException());
}
Fork/Join框架适合能够进行拆分再合并的计算密集型(CPU密集型)任务。Fork/Join框架是一个并行框架,因此要求服务器拥有多CPU、多核,用以提高计算能力。
有时候同样完成一个任务,多线程还不如单线程快,比如说在单核的CPU上,多线程的上下文切换也是一笔不小的开销,所以在单核的CPU上多线程还不如单线程速度快。
1)仅可以使用fork和join操作作为同步机制,如果使用了其他的同步机制,当处于同步操作模式下时任务无法被执行。例如,如果你想让一个任务在Fork/Join框架内休眠一段时间,执行这个任务的工作线程在休眠的期间不会去执行其他任务。
2)任务不能抛出异常。
3)任务不能执行I/O操作。
Fork/Join框架及其性能介绍
Java并发编程五:Fork/Join框架介绍