Fork/Join是JDK1.7的新特性。这个框架被设计用来解决可以使用分而治之技术将任务分解成更小的问题。
1.概念介绍
参考链接:http://ifeve.com/fork-join-1/
1)fork和join如何理解呢?
fork就是创建分支的意思:如果任务大小小于我们能接受的大小,那线程直接执行;否则,我们会创建分支,由两个子线程来执行原任务,依次递归;
join就是线程等待的意思:父任务线程创建了子任务线程,他需要等待子任务线程执行完毕,返回最终结果。
2)主要的几个类:
ForkJoinPool:执行ForkJoin的线程池,实现了ExecutorService接口;
ForkJoinTask:抽象类,是ForkJoinPool池的任务基类。其有两个实现的子类:
RecursiveAction:针对没有返回结果的子任务集;
RecursiveTask:针对有返回结果的子任务集。
2.创建一个Fork/join池
参考链接:http://ifeve.com/fork-join-2/
3)Fork/join代码:在RecursiveAction中实现compute()方法,方法中的逻辑如下:
If (problem size < default size){
execute(tasks);
} else {
tasks=divide(task);
//一般会用invokeAll(task1,task2..)
}
RecursiveAction主要属性:
1)要操作的大对象。比如一个数组。
2)自定义线程不细分最小执行量(threshold)。比如子数组长度<100执行,100就是一个自定义的threshold。
3)要操作对象的指针,首指针+尾指针。因为每个子任务不可能将原大对象复制一遍去处理,所以每个子线程操作的同一个大对象,那就需要两个指针,指明操作的段
4)对一个产品数组进行价格更新的示例,来自上一个连接中的示例。下面是手敲:
(ForkJoinPool这个池可用被监视,但本机测试效果不理想。还没搞清那些方法的真正含义,搞不动了,等实际用时再看)
@SuppressWarnings("serial")
public class ForkJoinModTest extends RecursiveAction{
private List<Producer> ps;
private int startP;
private int endP;
public ForkJoinModTest(){}
public ForkJoinModTest(List<Producer> ps,int startP,int endP){
this.ps = ps;
this.startP = startP;
this.endP = endP;
}
private int threshold = 100; //线程细分处理的最小量
@Override
protected void compute() {
if(endP-startP < threshold) //可以直接处理
{
this.updatePrice();
}else //继续细分处理
{
int middle = (startP + endP) >>> 1; //除以2
//分成两个子任务
ForkJoinModTest task1 = new ForkJoinModTest(ps,startP,middle+1);
ForkJoinModTest task2 = new ForkJoinModTest(ps,middle+1,endP);
//用invokeAll()调用执行两个子任务
super.invokeAll(task1,task2);
}
}
private void updatePrice(){
for(;startP<endP;startP++){
Producer p = ps.get(startP);
p.setPrice(12); //价格统一调高2元
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
List<Producer> ps = genarateProducers(1000000); //1w个待更新的产品
//ForkJoinPool,默认线程池数量为处理器核数
ForkJoinPool fjPool = new ForkJoinPool();
//创建Fork/join任务
ForkJoinModTest fjTask = new ForkJoinModTest(ps,0,ps.size());
//将任务交给线程池执行
fjPool.execute(fjTask);
//上面的代码已经完成了工作任务。下面的代码用于监视ForkJoinPool工作情况
do{
System.out.printf("池中活动线程数量: %d\n",fjPool.getActiveThreadCount());
System.out.printf("偷窃算法数量: %d\n",fjPool.getStealCount());
System.out.printf("并行数量: %d\n",fjPool.getParallelism());
System.out.println();
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}while(!fjTask.isDone());
fjPool.shutdown(); //实际中使用完,也记得关闭线程池
if(fjTask.isCompletedNormally()){
System.out.println("价格更新成功!");
}
for (int i=0; i<ps.size(); i++){
Producer product=ps.get(i);
if (product.getPrice()!=12) {
System.out.printf("Product %s: %f\n",product.getName(),product.getPrice());
}
}
System.out.println("--------the end--------");
}
public static List<Producer> genarateProducers(long x){
List<Producer> l = new ArrayList<Producer>();
for(int i=0;i<x;i++){
Producer p = new ForkJoinModTest().new Producer();
p.setName("p"+i);
p.setPrice(10);
}
return l;
}
class Producer{
private String name;
private int price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
}
3.加入任务的结果(用RecursiveTask返回任务结果)
参考链接:http://ifeve.com/fork-join-3/
问题:
1)报错:java.util.concurrent.ExecutionException: java.lang.StackOverflowError
ForkJoin不会对堆栈进行控制,编写代码时注意方法递归不能超过jvm的内存,如果必要需要调整jvm的内存:在Eclipse中JDK的配置中加上 -XX:MaxDirectMemorySize=128(默认是64M)。改为128后不报栈溢出,但是报下一个错。
2)java.lang.NoClassDefFoundError: Could not initialize class java.util.concurrent.locks.AbstractQueuedSynchronizer$Node
最终问题解决:
编码错误。在DocumentTask和LineTask两个类的compute()方法中,int midd = (start + end) >>> 1;。但开始时写错了,写成int midd = (start + end) >>> 2;造成子任务的处理长度不平衡。所以报上面的错。修改后,上面的1)、2)的错都消失了。
手写代码参考:test\src\com\sitech\concurrent\forkjoin\recursivetask包
4.异步运行任务
参考链接:http://ifeve.com/fork-join-4/
异步运行任务是指:创建任务后,提交给ForkJoinPool异步执行,不等待子任务执行返回结果。
在ForkJoinPool中执行ForkJoinTask时,你可以使用同步或异步方式来实现。当你使用同步方式时,提交任务给池的方法直到提交的任务完成它的执行,才会返回结果。当你使用异步方式时,提交任务给执行者的方法将立即返回,所以这个任务可以继续执行。
当你使用同步方法,调用这些方法(比如:invokeAll()方法)的任务将被阻塞,直到提交给池的任务完成它的执行。这允许ForkJoinPool类使用work-stealing算法,分配一个新的任务给正在执行睡眠任务的工作线程。反之,当你使用异步方法(比如:fork()方法),这个任务将继续它的执行,所以ForkJoinPool类不能使用work-stealing算法来提高应用程序的性能。在这种情况下,只有当你调用join()或get()方法来等待任务的完成时,ForkJoinPool才能使用work-stealing算法。
连接例子:搜索一个目录下,包括子目录中,文件为特定后缀的文件名,并打印。
例子中用的就是task.fork(),这个语句不会阻塞。所以当需要获取list结果集时,有可能搜索还为结束,需要用task.join()来等待并获取结果。
5.在任务中抛出异常
参考链接:http://ifeve.com/fork-join-5/
这章将的是在RecursiveTask的compute()方法中不能抛出已检测异常,如IOException或ClassNotFoundException等。即便实际运行时抛出了这些异常,程序也不会停止。所以在程序运行结束后,可以用:task.isCompletedAbnormally()来判断任务是否抛出异常(true表示抛出过异常),如果为true,则task.getException()来获取异常信息。
用ForkJoinTask.completeExceptionally()方法可完成Fork/Join异常处理。
6.取消任务
参考链接:http://ifeve.com/fork-join-6/
当你在一个ForkJoinPool类中执行ForkJoinTask对象,在它们开始执行之前,你可以取消执行它们。
但是在ForkJoinTask类中并不知道何时执行cancle()方法,而且每个任务都可能有喝多子任务,如果我们需要准确的取消某些还未提交的任务,就需要一个自定义的任务容器,将任务关联起来,然后试着执行某些任务的cancle()方法来取消任务执行。
关于Fork/Join的使用说明还有很多,可以参考上面链接页面中的扩展链接。暂时记录这么多,后续用的在查看。