java 线程溢出_java - Java和计算中的线程 - 堆栈内存溢出

多线程通常也意味着您想更快地完成一些工作。 因此,首先值得回顾一下您的初始设计,并使其在单线程上更快。 然后,这是一个目标。 另外,为了在不编写精确基准的情况下比较运行时间,您需要“可见”长度的运行时间。

在我的机器上,使用“设置”

int max = 1_000_000_000;

boolean sieve[] = new boolean[max];

long sum = 0; // will be 24739512092254535 at the end

您的原始代码,

for(int i=2;i

if(!sieve[i]) {

for(int j=i*2;j

sieve[j]=true;

sum+=i;

}

持续24-28秒。 正如@Andreas帖子下方评论中所讨论的,以及稍后的内容(是的,现在我看到它被接受并且大部分讨论都已经过去了),内部循环进行了许多额外的检查(因为它始终进行一次比较,即使它实际上不会启动)。 因此,外循环可以分为两部分:首先进行筛选和求和(直到max的最后一个“未知”除数,不超过其平方根),然后对其余部分求和:

int maxunique=(int)Math.sqrt(max);

for(int i=2;i<=maxunique;i++)

if(!sieve[i]) {

for(int j=i*2;j

sieve[j]=true;

sum+=i;

}

for(int i=maxunique+1;i

if(!sieve[i])

sum+=i;

这个在我的机器上运行14-16秒。 重大收获,尚未涉及任何线程。

然后出现线程,以及if(!sieve[i]) :在计算总和时,此类检查一定不能在内部循环之前发生,因为素数低于i素数超过了i ,所以sieve[i]确实告诉它是否是素数。 因为例如,如果某个线程正在运行,例如for(int i=4;i<10001;i+=2)sieve[i]=true; ,而另一个线程同时检查sieve[10000] ,它仍然会为false ,并且10000将被误认为是质数。

第一次尝试可能是在一个线程上进行筛选(无论如何,其外循环“仅”都将转到max平方根),然后并行求和:

for(int i=2;i<=maxunique;i++)

if(!sieve[i])

for(int j=i*2;j

sieve[j]=true;

int numt=4;

Thread sumt[]=new Thread[numt];

long sums[]=new long[numt];

for(int i=0;i

long ii=i;

Thread t=sumt[i]=new Thread(new Runnable() {

public void run() {

int from=(int)Math.max(ii*max/numt,2);

int to=(int)Math.min((ii+1)*max/numt,max);

long sum=0;

for(int i=from;i

if(!sieve[i])

sum+=i;

sums[(int)ii]=sum;

}

});

t.start();

}

for(int i=0;i

sumt[i].join();

sum+=sums[i];

}

这有点整洁,所有线程(我有4个内核)检查相同数量的候选对象,并且结果更快。 有时会快一秒钟,但通常会缩短一半(〜0.4 ...〜0.8秒)。 因此,这确实不值得付出努力,筛分循环是此处最耗时的部分。

可以决定允许多余的工作,并为筛子中遇到的每个素数编号启动一个线程,即使它不是实际的素数,也尚未被剔除:

List threads=new ArrayList<>();

for(int i=2;i<=maxunique;i++)

if(!sieve[i]) {

int ii=i;

Thread t=new Thread(new Runnable() {

public void run() {

for(int j=ii*2;j

sieve[j]=true;

}

});

t.start();

threads.add(t);

}

//System.out.println(threads.size());

for(int i=0;i

threads.get(i).join();

for(int i=maxunique+1;i

if(!sieve[i])

sum+=i;

注释过的println()会告诉(在我的机器上)创建了3500-3700个线程(而如果有人在原始循环中放入一个计数器,事实证明3401是最小的,那么在单个循环中会遇到很多素数) -线程筛循环)。 尽管过冲不会造成灾难性的影响,但线程数非常高,并且增益也不算太出色,尽管它比上一次尝试更明显:运行时间为10-11秒(当然可以降低一半)通过使用并行求和循环获得更多的秒数)。

当发现循环过滤非素数时,可以通过关闭循环来解决一些冗余工作:

for(int j=ii*2;j

这实际上起到了一些作用,对我来说,运行时间为8.6-10.1秒。

由于创建3401线程并不比创建3700线程少很多,因此限制它们的数量可能是个好主意,这是向Thread告别的地方。 尽管从技术上讲可以计算出它们的数量,但是有各种内置的基础结构可以为我们做到这一点。

Executors可以帮助将线程数限制为固定数量( newFixedThreadPool() ),或者甚至更好的是,将线程数限制为可用的CPU数量( newWorkStealingPool() ):

ExecutorService es=Executors.newWorkStealingPool();

ExecutorCompletionService ecs=new ExecutorCompletionService(es);

int count=0;

for(int i=2;i<=maxunique;i++)

if(!sieve[i]) {

int ii=i;

count++;

ecs.submit(new Callable() {

public Object call() throws Exception {

// if(!sieve[ii])

for(int j=ii*2;j

sieve[j]=true;

return null;

}

});

}

System.out.println(count);

while(count-->0)

ecs.take();

es.shutdown();

long sum=0;

for(int i=2;i

if(!sieve[i])

sum+=i;

这样,它会产生与上一个(8.6-10.5s)相似的结果。 但是,对于较少的CPU数量(4个内核),条件交换会导致一定的加速(取消注释if和在/**/之间的循环中注释相同的条件),因为任务按提交顺序运行,因此大多数冗余循环可以从一开始就退出,从而使重复检查浪费时间。 那对我来说是8.5-9.3s,超过了直接线程尝试的最佳和最差时间。 但是,如果您有很高的CPU数量(根据Runtime.availableProcessors() ),我也在32位可用的超级计算节点上也运行了它),则任务将重叠更多,并且重叠的是未分类的版本(因此,该任务总是进行检查)将会更快。

而且,如果您希望以较小的速度提高可读性,则可以使用流并行化内部循环(使用Thread也是可能的,非常繁琐):

long sum=0;

for(int i=2;i<=maxunique;i++)

if(!sieve[i]) {

sum+=i;

int ii=i;

IntStream.range(1, (max-1)/i).parallel().forEach(

j -> sieve[ii+j*ii]=true);

}

for(int i=maxunique+1;i

if(!sieve[i])

sum+=i;

这非常类似于原始的优化循环对,但对我来说仍然有9.4-10.0秒的速度。 因此它比其他方法慢(约10%左右),但要简单得多。

更新:

我修复了一系列xy

创建无数个子任务困扰着我,幸运的是发现了一个倒置的设计,在这里我们不检查是否达到sqrt(max) (这是maxunique ),但是相反,我们知道如果我们完成了对某个特定数字以下的数字的筛选limit ,我们可以继续检查数字直到limit*limit ,因为在范围内( limit ... limit*limit )内仍然是质数的实际上是质数(并且我们仍然要记住,这个上限受maxunique限制)。 这样就可以并行筛选。

基本算法,仅用于检查(单线程):

ExecutorService es=Executors.newWorkStealingPool();

ExecutorCompletionService ecs=new ExecutorCompletionService<>(es);

int limit=2;

int count=0;

do {

int upper=Math.min(maxunique+1,limit*limit);

for(int i=limit;i

if(!sieve[i]) {

sum+=i;

int ii=i;

count++;

ecs.submit(new Callable() {

public Object call() throws Exception {

for(int j=ii*2;j

sieve[j]=true;

return null;

}

});

}

while(count>0) {

count--;

ecs.take();

}

limit=upper;

} while(limit<=maxunique);

es.shutdown();

for(int i=limit;i

if(!sieve[i])

sum+=i;

由于某种原因,它比原始的两个循环变量稍慢(13.8-14.5秒vs 13.7-14.0秒,最小/最大20个运行),但是无论如何,我还是对并行化感兴趣。

可能由于质数的不均匀分布,使用并行流效果不佳(我认为这只是将工作按看似相等的方式预先划分),但是基于Executor的方法效果很好:

ExecutorService es=Executors.newWorkStealingPool(); ExecutorCompletionService ecs=new ExecutorCompletionService<>(es); int limit=2; int count=0; do { int upper=Math.min(maxunique+1,limit*limit); for(int i=limit;i() { public Object call() throws Exception { for(int j=ii*2;j0) { count--; ecs.take(); } limit=upper; } while(limit<=maxunique); es.shutdown(); for(int i=limit;i

对于低CPU数量的环境,这是迄今为止最快的环境(7.4-9.0秒与“无限线程”的8.7-9.9秒和其他基于Executor的环境的8.5-9.2秒)。 但是,一开始它运行的并行任务数量很少(当limit=2 ,它仅启动两个并行循环,分别针对2和3),最重要的是,这些循环运行时间最长(步数最小) ,因此,在高CPU数量的环境中,它仅比原来的基于Executor的驱动Executor落后2.9-3.6秒和2.7-3.2秒,仅排在第二位。

当然,一个人可以在开始时实施单独的加速,明确地收集必要数量的素数以使可用核饱和,然后切换到基于limit的方法,然后无论数量多少,结果都可能胜过其他核心。 但是我认为我现在可以抵制这种诱惑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值