You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程实现埃氏筛时核心数增多反而变慢的问题求助

多线程埃氏筛核心数增加反而变慢?问题分析与解决办法

这问题我之前在优化素数筛的时候也踩过坑!核心数加了反而跑不快,本质是共享资源竞争和任务粒度不合理在拖后腿,咱们一步步拆解解决:

为什么核心数越多越慢?

1. 共享数组的竞争开销爆炸

你用的AtomicBoolean数组(或普通数组加锁)是所有线程的竞争热点——每个线程标记非素数时都要读写同一个数组,核心线程越多,缓存一致性协议的同步开销、锁竞争的阻塞时间就越大,直接抵消了并行计算的收益,甚至让整体速度比单线程还慢。

2. 任务粒度太细导致调度过载

你给每个i单独开一个任务,如果n不算特别大,每个任务的执行时间极短(比如标记几个倍数就结束了),线程池的调度开销(线程切换、任务队列操作)会远远超过并行带来的效率提升,核心数越多,调度的额外开销就越高。

3. 线程池参数设置不合理

你的线程池核心数6、最大10,还配了容量为n的ArrayBlockingQueue——非核心线程的创建销毁、队列的频繁入队出队都会带来额外开销,而且这种场景下非核心线程完全没必要,反而增加复杂度。


具体解决办法

1. 合并任务,增大粒度

不要给每个i单独开任务,而是把2到sqrt(n)的范围拆成和核心数匹配的大块,每个线程处理一个区间内的所有i的标记工作,大幅减少任务调度次数:

// 获取CPU核心数,以此为基准拆分任务
int coreCount = Runtime.getRuntime().availableProcessors();
int sqrtN = (int) Math.sqrt(n);
int interval = sqrtN / coreCount;

// 用固定大小线程池更合适,避免非核心线程的开销
ExecutorService executor = Executors.newFixedThreadPool(coreCount);

for (int j = 0; j < coreCount; j++) {
    int start = 2 + j * interval;
    // 最后一个区间覆盖到sqrt(n)
    int end = (j == coreCount - 1) ? sqrtN : start + interval;
    executor.execute(new BatchPrimeTask(start, end, n, list));
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.HOURS);

// 批量任务的run方法
class BatchPrimeTask implements Runnable {
    private int start, end, n;
    private boolean[] list; // 换成普通boolean数组,配合后续分段锁优化

    public BatchPrimeTask(int start, int end, int n, boolean[] list) {
        this.start = start;
        this.end = end;
        this.n = n;
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = start; i <= end; i++) {
            if (list[i]) { // 确认当前i是素数
                // 标记i的所有倍数
                for (int k = i * i; k < n; k += i) {
                    list[k] = false;
                }
            }
        }
    }
}

2. 减少共享资源竞争:分段锁或数组拆分

如果还是用共享数组,别用全局锁或者AtomicBoolean,改用分段锁:把数组分成若干段,每段对应一把锁,线程处理某段的倍数时只需要获取对应段的锁,不同段的线程互不干扰:

// 比如每1024个元素为一段,创建对应的锁数组
int segmentSize = 1024;
Lock[] locks = new Lock[(n + segmentSize - 1) / segmentSize];
for (int i = 0; i < locks.length; i++) {
    locks[i] = new ReentrantLock();
}

// 在标记倍数时,获取对应段的锁
int segmentIndex = k / segmentSize;
locks[segmentIndex].lock();
try {
    list[k] = false;
} finally {
    locks[segmentIndex].unlock();
}

更彻底的方式是拆分数组:每个线程维护自己的局部数组,处理完后再合并到全局数组,完全避免竞争,但需要额外的合并逻辑,适合超大n的场景。

3. 改用分段筛(Segmented Sieve)优化并行效率

如果n特别大(比如超过1e8),普通埃氏筛的内存开销会很高,而且共享数组的竞争无法避免。这时候可以用分段筛:把整个范围分成多个小的段,每个线程处理一段,先预先生成sqrt(n)以内的素数,再用这些素数去标记每个段里的非素数。这种方式每个线程的工作内存局部性更好,缓存命中率高,几乎没有竞争。

4. 加个阈值判断:小n用单线程

当n比较小(比如小于1e6),单线程的缓存局部性优势完全盖过多线程的并行收益,这时候直接用单线程筛法反而更快。可以加个判断:

if (n < 1_000_000) {
    // 单线程埃氏筛
    singleThreadSieve(n, list);
} else {
    // 多线程版本
    multiThreadSieve(n, list);
}

内容的提问来源于stack exchange,提问作者murksiuke

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:03:53