前言:
在高并发方面,作为仍然活跃在各大服务器上的主流语言之一,Java因其不错的性能以及各类高性能并发框架的支持,依然有着顽强的生命力。
学习编程如同练功,一切都得从基础开始。想要Java玩的溜,一些基础的知识你少不了,本文将介绍一些Java并行计算的基础知识。
首先,我们得了解,什么是并行计算。
并行计算或称平行计算是相对于串行计算来说的。
它是一种一次可执行多个指令的算法,目的是提高计算速度,及通过扩大问题求解规模,解决大型而复杂的计算问题。所谓并行计算可分为时间上的并行和空间上的并行。 时间上的并行就是指流水线技术,而空间上的并行则是指用多个处理器并发的执行计算。
总而言之,并行计算的任务多,数据量大 ,对比之下,串行编程简单,而并行编程困难。但是由于当前计算机CPU发展趋势已不再是一味的提高频率,而是着重于增加计算核的数量,这也使得当前CPU单个计算核频率逐渐下降,核数增多,整体性能变高 。一定程度上迫使我们更加熟练的运用并行计算。
然而,并行计算有很多困难(任务分配和执行过程高度耦合) ,例如,我们应当如何控制任务划分,如何合理高效的切割任务,使得各个计算核的负载能够尽可能地达到平衡 ,如何分配任务给各线程,同时监督线程执行过程。
当前的并行模式,主要有:
–主从模式(Master-Slave)
–Worker模式(Worker-Worker)
为了实现这些业务需求,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);
}
}
运行结果 如下:
从上面的结果,我们发现对于之前的并行计算的痛点,线程组还未能有效解决:
–能够有效管理多个线程,但是管理效率低 ,管理层次较低
–任务分配和执行过程高度耦合
–重复创建线程、关闭线程操作,线程组依然通过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;
}
}
这样看来,executor框架对于解决问题有了很大的帮助。能够有效管理多个线程,同时很大程度上,解决了任务分配和执行过程高度耦合的问题,提高了并行编程的效率。
下面介绍了一种并发框架,Fork Join框架,
Fork-Join框架
考虑一种情况,当计算任务大小不确定时,即整体任务量不好确定的,但是细分到最小的任务,我们可以很快的确定。这时候,我们通常采用分而治之的思想,而ForkJoin就是这样实现的。
常用类:
–ForkJoinPool 任务池
–RecursiveAction 定义具体的任务
–RecursiveTask
下面来看一下具体的代码实现:
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;
}
}
从运行的过程和结果中,我们可以看出,线程的数量在动态的变化着,最终求得了具体的结果:
总结:
本文介绍了ThreadGroup,Executor和Fork Join这三个较为基础的多线程使用方法,其中Executor和Fork Join的管理效率较高,但是需要注意这两个框架适用的情况。
Executor适用于任务量便于确定的情况,而ForkJoin则适用于任务不确定,大小不好评估的任务情况(上述加法实例其实很好确定任务大小,只是为了叙述方便,在两个不同的框架下分别介绍使用)