问题原型:求三千万以内的素数.
问题的解决办法很简单,写一个求素数算法 然后循环就可以.这个是顺序执行方式,下面附上代码
public abstract class PrimeAbstract { public boolean isPrime(int i){ if(i<=1)return false; else{ for(int j=2; j<=Math.sqrt(i);j++){ if(i%j == 0)return false; } return true; } } public static void print(Object a){ System.out.println(a); } public int countPrimeInRange(int low, int up){ int total = 0; for(int i=low; i<=up; i++){ if(isPrime(i))total++; } return total; } public void timeAndCompute(final int number){ final long start =System.nanoTime(); final long num = countPrimes(number); final long end = System.nanoTime(); print("小于"+number+"的素数的个数为"+num+"花费的时间"+(end-start)/1.0e9); } public abstract int countPrimes(final int max); }
上面是一个抽象类,大致就是定义了几种求解素数的工具,求一个范围的素数,还有计时,还有求解整个范围素数的方法,而这个方法是抽象的也是我们需要实现的,也是并行和非并行的不同之处.
顺序执行的方法:
class primeshunxu extends PrimeAbstract{ @Override public int countPrimes(int max) { // TODO Auto-generated method stub return countPrimeInRange(0, max); } }
很简单,已经有现成的工具了嘛,直接用就好.
并行的方法:
并行的精髓是做任务分割,具体怎么分割上面其实已经表现出来了,讲整个求解范围进行分割,然后把求解结果累加即可.下面是代码
import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Prime extends PrimeAbstract{ @Override public int countPrimes(int max) { // TODO Auto-generated method stub final int poolsize = (int) (Runtime.getRuntime().availableProcessors()); final ExecutorService executorpool = Executors.newFixedThreadPool(poolsize); final List<Callable<Integer>> partition = new ArrayList<>(); int total = 0; int tasks =poolsize; int per = max/tasks; int pre = 0; for(int j = 1; j<=tasks; j++){ final int low = pre+1; final int up; if(low+per>max){ up = max; }else{ up = low+per; } partition.add(new Callable<Integer>() { @Override public Integer call() throws Exception { // TODO Auto-generated method stub return countPrimeInRange(low, up); } }); pre = up; } try { List<Future<Integer>> result = executorpool.invokeAll(partition); executorpool.shutdown(); for(Future<Integer> i : result){ total += i.get(); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return total; } }
最后调用
public static void main(String[] args) { // TODO Auto-generated method stub int max = 3000000; new Prime().timeAndCompute(max); new primeshunxu().timeAndCompute(max); }
现在我看看下运行对比
小于3000000的素数的个数为216816花费的时间6.464452757
小于3000000的素数的个数为216816花费的时间12.226215544
可以看到并行的时间是顺序执行的一半,虽然代码上复杂了很多,但是效果还是比较明显的,这里就涉及到了poolsize和tasks的个数问题:
根据poolsize计算公式,对于计算密集型任务,阻塞系数是0,那么poolsize大小设置成和cpu核数相等即可,因为假如设置大了以后,cpu就会挂起一个非阻塞线程,然后去执行另一个非阻塞任务,这样做意义不大,而且会代码额外的上下文切换开销.也就是在cpu密集型任务中,通过将线程数增多到比核数还多,是无效的.
那么剩下的就是tasks了.
我们首先看tasks = poolsize的情况,也就是将每个线程执行一个任务,然后就结束
看下分析结果
执行输出
小于3000000的素数的个数为216816花费的时间6.24529681
可以看到 有四个线程,因为本机的核数为4,然后其中三个线程在运行完毕后进入了驻留状态,而还有一个线程从开始到最后一直在执行,也就是那个数值分配最大的那个线程,由于计算花费的时间更多,所以最好只有这单个线程执行了.所以运行时间也就是运行时间最长的那个线程所花的时间
那么我们可以想到,这样来分配任务的话,造成了四个线程的负载时不均衡的,计算量最大的线程是时间瓶颈,那么我们可以怎么做呢?
1.在任务数不变的情况下重新划分,可以想象前面12.5%的数计算量很小,后12.5%计算量很大,这前面的八分之一和后面的八分之一组合成一个任务,这样来进行均衡,但是这样做要对问题本身的行为要有一个非常深刻的理解,实现相对麻烦
2.增加任务数,我们可以想象,当我们任务数变多的时候,每次四个线程上跑的都是时间花销差不多的任务,这样一来,我们就能够做到任务均衡了.但是具体增加到多少呢?
我们增加到100个任务
运行结果:
小于3000000的素数的个数为216816花费的时间5.921416693
可以看到,有了一些提升,而且每个线程从开始到最后都是满负载的,做到了负载均衡.这里虽然没有理论上的达到顺序执行的四分之一,也就是四秒左右,但是已经很客观了.
关于poolsize和tasks的总结:
1.子任务的划分数是不能小于处理器的核心数,否则性能都很差的.
2.对于计算密集型任务,线程数多于cpu核心数对性能提升是毫无帮助的.
3.在子任务数超过一定的数量后,再继续增加的话,对性能提升是十分有限的