ForkJoin框架的分而治之

分而治之的思想不陌生了,归并排序用到,即把待排序列递归地等分成两个子序列,并排序,这样在不停递归等分,排序归并过程中,会得到若干个有序序列,因为递归过程是等分成左右序列嘛,所以最后会得到两个有序的序列,再做一次归并操作即可。外部排序中也用到,现在在多线程中也有,在多线程中分而治之思想有基于数据的分割和基于任务的分割。拿基于数据的分割来说,例如对于一个大文件的下载,使用多个线程分别下载一个大文件的不同部分,比用单线程单独下载一个大文件节省很多的时间,等下会用代码举例。

在Java中也有实现这种分工合作思想的工具:Fork/Join框架。fork()功能是创建分支线程,join()功能一样是线程等待的意思,需要等待调用join()的线程执行完成后才能继续。因为fork()和join两个方法都是ForkJoinTask类的方法,所以在一个Fork/Join’框架下,由ForkJoinTask类来表示任务。

JDK中提供个ForkJoinPool线程池来对fork()创建的分支线程做管理。这个ForkJoinPool里的线程池有一个特点,就是当线程池里面的fork()线程完成自己的手头工作后,会去主动帮助其他还未完成任务的fork()线程执行它未完成的任务。因为在实际应用中,一个线程是需要完成多个任务的,每个线程都有自己的任务队列,里面放着等待这个线程调度执行的任务,当线程T1执行完自己的所有任务后,会去查看其他线程例如T2的任务队列,假设线程T2的任务队列中有很多任务等待着处理,那么T1就会从T2的任务队列最底端开始抽取任务来帮助线程T2的执行,这种从最底端开始抽取任务来执行的行为可以尽可能避免资源竞争,帮助线程执行任务行为也能提高CPU利用率。

好了,大概知道fork()方法用来把任务创建分支,和用join()等待任务完成并返回结果。这两个方法都是ForkJoinTask类的方法,也就是说要用fork/join框架,就要使用ForkJoinTask类来实现,ForkJoinTask类有两个子类,RecursiveAction类和RecursiveTask类,想要使用Fork/Join框架的任务都应该继承自这两个类中的其中一个来实现(翻看了片文档,Fork/Join),继承自RecursiveAction类的任务表示该任务没有返回值;继承自RecursiveTask类的任务表示该任务有返回值。来看看具体实现代码:

package forkjoinframe;

import java.util.ArrayList;
import java.util.concurrent.RecursiveTask;

public class ForkJoinDownloadTask extends RecursiveTask {
	
	private static final long serialVersionUID = 1L;
	private long lowerBound; //起始字节
	private long upperBound; //结束字节
	private long dataSum; //(子)任务数据量
	private long sum = 0;
	
	public ForkJoinDownloadTask(long lowerBound, long upperBound) {
		this.lowerBound = lowerBound;
		this.upperBound = upperBound;
	}
	
	@Override
	protected Long compute() {
		
		if((upperBound-lowerBound)<10000) {
			for(long i=lowerBound; i<=upperBound; i++) {
				sum +=i; //模拟下载数据
			}
		} else {
			//如果文件太大,就进行分割处理
			long part = (upperBound-lowerBound)/100; //记录分割成100部分,每部分文件的大小
			long startLocation = lowerBound; //initLocation记录任务的初始开始位置
			//存放这100个子任务的队列
			ArrayList downloadTask = new ArrayList(); 
			
			//把分成的100个子任务都提交到队列里去
			for(int i=0; i<100; i++) {
				long endLocation = startLocation+part; //记录该子任务的下载结束位置
				//特殊情况判断,如果该子任务的结束位置大于总任务的结束位置,即当前子任务是最后一个了.
				if(endLocation>upperBound) {
					//让子任务的结束位置等于总任务大小的结束位置.
					endLocation = upperBound;
				}
				
				//实例化子任务
				ForkJoinDownloadTask downloadtask = new ForkJoinDownloadTask(startLocation, endLocation);
				//输出子任务的信息
				downloadtask.dataSum = downloadtask.upperBound - downloadtask.lowerBound; //该子任务的下载数据量
				sum+=downloadtask.dataSum; //sum保存数据总量
				System.out.println("Task - "+i+": startLocation="+downloadtask.lowerBound+", "
						+ "endLocation="+downloadtask.upperBound);
				System.out.println("dataSum="+downloadtask.dataSum+"  sum="+sum+"\r\n");
								
				startLocation+=part; //下一个子任务的开始位置
				downloadTask.add(downloadtask); //把当前子任务存入任务队列
				downloadtask.fork(); //为子任务创建分支
			}
			//所有子任务提交到任务队列完成
			
			for(ForkJoinDownloadTask t:downloadTask) {
				t.join(); //join()等待任务,即"等待子任务下载完成"
			}
		}
		
		return sum;
	}
}

这段代码是模拟下载一个大文件,任务FrokJoinDownloadTask继承RecursiveTask类,表示该任务有返回值,因为我们想要在每个子任务完成任务后输出下自己“下载”了多少数据,返回自己的”下载”数据量后也可以用来统计总的完成下载数据量。

      继承RecursiveTask类后,就要重写它的compute方法实现要执行的任务,在这个模拟下载任务中,首先判断下载的数据量大小,如果要下载的数据量小于10000,就直接“下载“可以了,否则大于10000,就把它分解成100个子任务来分别下载这些数据量。注意第34行创建了一个子任务队列,给每个子任务fork()时也把子任务存放到这个子任务队列里去,然后第60行我们把这子任务队列中的所有子任务都调用join()方法。最后第65行会返回sum变量,sum记录着当前下载的总数据量。

package forkjoinframe;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;

public class ForkJoinFrameDemo {

	public static void main(String[] args) {
		ForkJoinPool forkjoinpool = new ForkJoinPool(); //ForkJoinPool线程池
		ForkJoinDownloadTask task = new ForkJoinDownloadTask(0, 300000L); //实例化下载任务
		ForkJoinTask taskResult = forkjoinpool.submit(task); //存放下载任务返回的结果
		
		try {
			long result = taskResult.get(); //获取每个子任务的下载结果
			System.out.println("completed! sum = "+result);
		} catch(InterruptedException e) {
			e.printStackTrace();
		} catch(ExecutionException e) {
			e.printStackTrace();
		}

	}

}

在实例化任务和调用任务的类中,第12行我们用一个ForkJoinTask队列来保存ForkJoinTask任务的返回值,也就是那个记录下载总数据量的sum。

可以看到,对于下载一个数据量为300000任务被分成了编号为0-99的100个子任务,每个任务下载数据量为3000。

      看了一些文档,ForkJoinPool线程池中的线程是由ForkJoinWorkerThread类实现的,线程池存放继承ForkJoinTask子类的任务是由一个ForkJoinPool.WorkQueue双端队列来存放,以此来实现线程之间的互相帮助,也就是上面提到的,对于同一个任务的子任务线程,在某个线程完成自己的所有任务(包括该线程的任务队列中的任务)后,会去帮助其他线程完成其任务队列中的等待任务。

详细代码已上传:

https://github.com/justinzengtm/Java-Multithreading/tree/master/ForkJoinFrame

https://gitee.com/justinzeng/multithreadthread_pool/tree/master/ForkJoinFrame

你可能感兴趣的:(多线程,Java)