为什么一个线程执行速度比许多线程快得多?

时间:2017-03-08 09:24:24

标签: java multithreading concurrency executorservice

我在生产代码中遇到过这个问题,这是一个简化的例子:

public static void main(String[] args) throws ExecutionException, InterruptedException {

    long start = System.currentTimeMillis();

    ExecutorService executor = Executors.newFixedThreadPool(1); // slower for > 1
    List<CompletableFuture<Void>> futures = new ArrayList<>();

    for (int i = 0; i < 10; i++) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        futures.add(future);
        executor.submit(() -> {
            int sum = 0; // prevent compiler to get rid of the loop
            for (int j = 0; j < 1_000; j++) {
                String random = RandomStringUtils.randomAlphanumeric(100, 10_000);
                sum += random.length();
            }
            System.out.println(Thread.currentThread().getName() + " sum: " + sum);
            future.complete(null);
        });
    }

    executor.shutdown();

    // prevent program to exit before branched threads complete
    for (CompletableFuture<Void> future : futures) {
        future.get();
    }

    System.out.println("Completed in: " +
            (System.currentTimeMillis() - start));
} 

TL; DR:我只使用apache-commons RandomStringUtils生成一些字符串。没有明确的同步。

我的问题是,当我增加FixedThreadPool中的线程数时,为什么代码执行得慢得多?

以下是1和10个线程的结果(在8核超线程cpu上测试):

1个帖子:

pool-1-thread-1 sum: 5208706
pool-1-thread-1 sum: 4934655
pool-1-thread-1 sum: 5173253
pool-1-thread-1 sum: 5016372
pool-1-thread-1 sum: 4949229
pool-1-thread-1 sum: 5267758
pool-1-thread-1 sum: 5156963
pool-1-thread-1 sum: 5112007
pool-1-thread-1 sum: 4986156
pool-1-thread-1 sum: 4916637
Completed in: 1431

10个帖子:

pool-1-thread-6 sum: 4928768
pool-1-thread-10 sum: 4946490
pool-1-thread-5 sum: 4955353
pool-1-thread-8 sum: 5043251
pool-1-thread-3 sum: 5125496
pool-1-thread-4 sum: 5045113
pool-1-thread-2 sum: 5040489
pool-1-thread-1 sum: 5123954
pool-1-thread-9 sum: 5090715
pool-1-thread-7 sum: 5399434
Completed in: 11547

所以它比10个线程慢了x10倍。两个线程比一个线程慢〜x1.5倍。

我熟悉阿姆达尔定律。但我不确定,是这样吗?在我看来,这种工作应该很容易并行化。

1 个答案:

答案 0 :(得分:4)

我怀疑它不能很好地扩展的原因在于Apache的代码。

我发现RandomStringUtils使用的标准java.util.Random已知不能很好地扩展多个线程,因为这种代码:

protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
}

这会为AtomicLong使用seed。换句话说,所有线程都使用相同的Random实例,它使用相同的AtomicLong。这将导致线程之间的争用(特别是因为你生成了这么长的随机字符串)并且它们会浪费很多周期来进行不必要的同步。

当我使用不同类型的CPU消耗函数(一个求和的循环)测试它时,多个线程的缩放按预期工作。