前言:

在高并发方面,作为仍然活跃在各大服务器上的主流语言之一,Java因其不错的性能以及各类高性能并发框架的支持,依然有着顽强的生命力。
学习编程如同练功,一切都得从基础开始。想要Java玩的溜,一些基础的知识你少不了,本文将介绍一些Java并行计算的基础知识。

首先,我们得了解,什么是并行计算。

并行计算或称平行计算是相对于串行计算来说的。
它是一种一次可执行多个指令的算法,目的是提高计算速度,及通过扩大问题求解规模,解决大型而复杂的计算问题。所谓并行计算可分为时间上的并行和空间上的并行。 时间上的并行就是指流水线技术,而空间上的并行则是指用多个处理器并发的执行计算。

总而言之,并行计算的任务多,数据量大 ,对比之下,串行编程简单,而并行编程困难。但是由于当前计算机CPU发展趋势已不再是一味的提高频率,而是着重于增加计算核的数量,这也使得当前CPU单个计算核频率逐渐下降,核数增多,整体性能变高 。一定程度上迫使我们更加熟练的运用并行计算。

然而,并行计算有很多困难(任务分配和执行过程高度耦合) ,例如,我们应当如何控制任务划分,如何合理高效的切割任务,使得各个计算核的负载能够尽可能地达到平衡 ,如何分配任务给各线程,同时监督线程执行过程。

当前的并行模式,主要有:

–主从模式(Master-Slave)
–Worker模式(Worker-Worker)

java 并行计算excutor返回结果终止 java并行计算框架_线程组


java 并行计算excutor返回结果终止 java并行计算框架_java_02


为了实现这些业务需求,Java基础的有这几个:
–Thread/Runnable/Thread组管理
–Executor框架
–Fork-Join框架

Thread组管理:

前文中介绍到Java多线程的基本创建以及使用,然而当线程数目较大时,程序员对其的管理将变得繁琐且低效。因此,Java为开发者提供了一个线程组的管理方法,ThreadGroup,其具有以下特点:
–可视为线程的集合
–树形结构,大线程组可以包括小线程组
–可以通过enumerate方法遍历组内的线程,执行操作

下面,我们来结合一个实例看一看:

import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class ThreadgroupTest {

	public static void main(String[] args) {

		// 创建线程组
		ThreadGroup threadGroup = new ThreadGroup("Searcher");
		Result result=new Result();

		// 创建一个任务,4个线程完成
		Searcher searchTask=new Searcher(result);
		for (int i=0; i<4; i++) {
			Thread thread=new Thread(threadGroup, searchTask);
			thread.start();
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("========分割线1=======");
		
		// 查看线程组消息
		System.out.printf("active 线程数量: %d\n",threadGroup.activeCount());  //activeCount返回线程组中还处于活跃状态的线程数目(估计值)
		System.out.printf("线程组信息明细\n");
		threadGroup.list();  //打印线程组中所有线程的信息
		System.out.println("========分割线2=======");

		// 遍历线程组
		Thread[] threads=new Thread[threadGroup.activeCount()];
		threadGroup.enumerate(threads);  //将线程中活跃的线程拷贝到线程组中
		for (int i=0; i<threadGroup.activeCount(); i++) {
			System.out.printf("Thread %s: %s\n",threads[i].getName(),threads[i].getState());
		}
		System.out.println("========分割线3=======");

		
		waitFinish(threadGroup);  // 等待线程完成
		threadGroup.interrupt();  //发出中断信号,使得组中所有线程中断
	}

	public static void waitFinish(ThreadGroup threadGroup) {
		while (threadGroup.activeCount()>9) {  //每隔一秒轮询线程组的情况
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
class Result {
	
	private String name;	
	public String getName() {  //获取昵称
		return name;
	}	
	public void setName(String name) {  //设置昵称
		this.name = name;
	}

}
class Searcher implements Runnable {
	private Result result;	
	public Searcher(Result result) {
		this.result=result;
	}
	@Override
	public void run() {
		String name=Thread.currentThread().getName();
		System.out.printf("Thread %s: 已启动\n",name);
		try {
			runTask();  //执行模拟任务
			result.setName(name);
		} catch (InterruptedException e) {
			System.out.printf("Thread %s: 被中断\n",name);
			return;
		}
		System.out.printf("Thread %s: 已完成\n",name);
	}	
	private void runTask() throws InterruptedException {
		Random random=new Random((new Date()).getTime());
		int value=(int)(random.nextDouble()*100);  //随机生成一个休眠的时间,模拟线程正在工作的情景
		System.out.printf("Thread %s: %d\n",Thread.currentThread().getName(),value);   //获取线程信息
		TimeUnit.SECONDS.sleep(value);
	}
}

运行结果 如下:

java 并行计算excutor返回结果终止 java并行计算框架_软件框架_03


从上面的结果,我们发现对于之前的并行计算的痛点,线程组还未能有效解决:

–能够有效管理多个线程,但是管理效率低 ,管理层次较低

–任务分配和执行过程高度耦合

–重复创建线程、关闭线程操作,线程组依然通过new产生线程,并且start之后,无法再次被start,导致无法重用线程 ,然而new产生线程的过程消耗比较大,性价比低。

下面来介绍一个自从JDK1.5版本开始提供的高性能框架———Executor

Executor框架

Executor FrameWork(java.util.concurrent.*) 具有以下优势:
–分离任务的创建和执行者的创建
–线程可以重复利用,有效利用已有的线程资源(因为new线程代价很大)

该框架中有共享线程池 ,即预设好的多个Thread,可弹性增加,多次执行很多很小的任务 ,运行结束后,线程处于 空闲状态,类似于共享单车,不用的时候处于空闲状态。同时任务创建和执行过程解耦 ,使得程序员无需关心线程池执行任务过程,框架内部会自动处理各类wait,sleep和block状态,减小了程序员的负担,给并行编程带来了极大的便利性。

• 该框架有以下几个主要的类:
ExecutorService, ThreadPoolExecutor,Future

–Executors.newCachedThreadPool / newFixedThreadPool创建线程池 (既可以创建由框架自动分配数量的线程池,也可以为固定数量的线程池)

–ExecutorService线程池服务,代表了整个线程池

–Callable 具体的逻辑对象(线程类) (与Runnable类类似,用于执行任务,同时,Runnable的run方法没有返回结果,而Callable的call方法有返回结果)

–Future 用于存放计算的结果
下面来看一个实例:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;

public class Executor_SumTest {

	public static void main(String[] args) {
		ThreadPoolExecutor executor =(ThreadPoolExecutor)Executors.newFixedThreadPool(4);
		List<Future<Integer>> resultList = new ArrayList<>();
		for(int i =0;i<10;i++) {
			SumTask calculator = new SumTask(i*1000+1,(i+1)*1000);
			Future<Integer> result = executor.submit(calculator);
			resultList.add(result);
		}  
		//轮询任务完成情况
		do {
			System.out.printf("Main:已完成多少任务:%d\n",executor.getCompletedTaskCount());
			for(int i=0;i<resultList.size();i++) {
				Future<Integer> result=resultList.get(i);
				System.out.printf("Main:Task%d: %s\n", i,result.isDone());
				
			}try {
				Thread.sleep(50);
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
			
		} while(executor.getCompletedTaskCount()<resultList.size());
		//所有任务已经结束了,综合计算结果
		int total =0;
		for(int i=0;i<resultList.size();i++) {
			Future<Integer> result=resultList.get(i);
			Integer sum =null;
			try {
				sum=result.get();
				total = total+sum;
			}catch(InterruptedException e) {
				e.printStackTrace();
			}catch(ExecutionException e) {
				e.printStackTrace();
			}
		}
		System.out.printf("1-10000的总和:"+total);
		executor.shutdown();  //关闭所有线程
	}
}
class SumTask implements Callable<Integer> {  //计算任务类,用于执行具体的任务
	private int startNumber;
	private int endNumber;
	
	public SumTask(int startNumber,int endNumber) {
		this.startNumber = startNumber;
		this.endNumber = endNumber;
	}
	@Override
	//累计结果
	public Integer call() throws Exception {
		int sum =0;
		for(int i=startNumber;i<=endNumber;i++) {
			sum += i;
		}
		System.out.printf("%s: %d\n", Thread.currentThread().getName(),sum);
		return sum;
	}
}

java 并行计算excutor返回结果终止 java并行计算框架_java_04


这样看来,executor框架对于解决问题有了很大的帮助。能够有效管理多个线程,同时很大程度上,解决了任务分配和执行过程高度耦合的问题,提高了并行编程的效率。

下面介绍了一种并发框架,Fork Join框架,

Fork-Join框架

考虑一种情况,当计算任务大小不确定时,即整体任务量不好确定的,但是细分到最小的任务,我们可以很快的确定。这时候,我们通常采用分而治之的思想,而ForkJoin就是这样实现的。
常用类:
–ForkJoinPool 任务池
–RecursiveAction 定义具体的任务
–RecursiveTask

java 并行计算excutor返回结果终止 java并行计算框架_多线程_05

下面来看一下具体的代码实现:

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

public class ForkJoin_SumTest {
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建执行线程池
    	ForkJoinPool pool = new ForkJoinPool();  //可以是系统按照当前计算机性能,实时动态,自动分配的数量
    	//ForkJoinPool pool = new ForkJoinPool(4);  //也可以是程序员设定具体的数量
    	
    	//创建任务,计算1-1000000000的和
        ForkJoinSumTask task = new ForkJoinSumTask(1, 1000000000);
        
        //提交任务
        ForkJoinTask<Long> result = pool.submit(task);
        
        //轮询,等待结果
        do {
			System.out.printf("Main: Thread Count: %d\n",pool.getActiveThreadCount());
			System.out.printf("Main: Paralelism: %d\n",pool.getParallelism());
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		} while (!task.isDone());      
        //输出结果
        System.out.println(result.get().toString());
    }
}
//分任务求和
@SuppressWarnings("serial")
class ForkJoinSumTask extends RecursiveTask<Long> {   //实现递归的任务
	private int start;
	private int end;
	public ForkJoinSumTask(int start, int end) {
		this.start = start;
		this.end = end;
	}
	public static final int threadhold = 5;  //设置任务大小的阈值
	@Override
	protected Long compute() {
		Long sum = 0L;
		
		// 如果任务足够小, 就直接执行
		boolean canCompute = (end - start) <= threadhold;
		if (canCompute) {
			for (int i = start; i <= end; i++) {
				sum = sum + i;				
			}
		} else {
			// 任务大于阈值, 分裂为2个任务
			int middle = (start + end) / 2;
			ForkJoinSumTask subTask1 = new ForkJoinSumTask(start, middle);
			ForkJoinSumTask subTask2 = new ForkJoinSumTask(middle + 1, end);
			invokeAll(subTask1, subTask2);
			Long sum1 = subTask1.join();
			Long sum2 = subTask2.join();
			// 结果合并
			sum = sum1 + sum2;
		}
		return sum;
	}
}

从运行的过程和结果中,我们可以看出,线程的数量在动态的变化着,最终求得了具体的结果:

java 并行计算excutor返回结果终止 java并行计算框架_多线程_06

总结:

本文介绍了ThreadGroup,Executor和Fork Join这三个较为基础的多线程使用方法,其中Executor和Fork Join的管理效率较高,但是需要注意这两个框架适用的情况。
Executor适用于任务量便于确定的情况,而ForkJoin则适用于任务不确定,大小不好评估的任务情况
(上述加法实例其实很好确定任务大小,只是为了叙述方便,在两个不同的框架下分别介绍使用)