为什么一个线程的执行速度比多个线程快得多?
Why does one thread performs much faster then many?
我在生产代码中遇到过这个,这里是一个简化的例子:
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 个线程,速度会慢 10 倍。两个线程的执行速度比一个慢 ~ x1.5 倍。
我熟悉阿姆达尔定律。但我不确定,是这样吗?在我看来,这种工作应该很容易并行化。
我怀疑它不能很好扩展的原因在于 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 消费函数(求和的循环)对其进行测试时,多线程的缩放按预期工作。
我在生产代码中遇到过这个,这里是一个简化的例子:
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 个线程,速度会慢 10 倍。两个线程的执行速度比一个慢 ~ x1.5 倍。
我熟悉阿姆达尔定律。但我不确定,是这样吗?在我看来,这种工作应该很容易并行化。
我怀疑它不能很好扩展的原因在于 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 消费函数(求和的循环)对其进行测试时,多线程的缩放按预期工作。