[英]Why Java multi-threading does not give a linear speedup to the number of threads
我的 Java 多线程代码如下。 我预计如果我使用n 个线程( n < 可用内核),总执行时间将是使用单个线程的 1/n。 但是实验结果并没有证明这一点:1线程4128 ms,2线程2200 ms,3线程3114 ms,4线程3031 ms。
public static void main(String[] args) {
List<Document> doc = createDocuments(50000);
int numThreads = 4;
List<List> partitionedDocs = partitionData(doc, numThreads);
CountDownLatch latch = new CountDownLatch(numThreads);
List<Thread> threads = new ArrayList<>();
List<Runnable> executors = new ArrayList<>();
for (List input : partitionedDocs) {
Runnable executor = new SplitDocuments(input, " ", latch);
executors.add(executor);
threads.add(new Thread(executor));
}
for (Thread t : threads) {
t.start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
对于 main 中使用的方法:
public static List<Document> createDocuments(Integer size) {
List<Document> x = new ArrayList<>();
String text = "";
for (int i=0; i<500; i++) {
text += "abc ";
}
for (int i=0; i<size; i++) {
Document doc = new Document(i, text);
x.add(doc);
}
return x;
}
private static List<List> partitionData(List input, int numThreads) {
List<List> results = new ArrayList<>();
int subListSize = input.size()/numThreads;
for (int i=0; i<numThreads; i++) {
if (i==numThreads-1) {
results.add(input.subList(i*subListSize, input.size()));
}
else {
results.add(input.subList(i*subListSize, (i+1)*subListSize));
}
}
return results;
}
对于 SplitDocuments class:
public class SplitDocuments implements Runnable {
private String splitter;
private List<Document> docs = new ArrayList<>();
private CountDownLatch latch;
private List<Document> result = new ArrayList<>();
public SplitDocuments(List<Document> docs, String splitter, CountDownLatch latch) {
this.docs = docs;
this.splitter = splitter;
this.latch = latch;
}
@Override
public void run() {
System.out.println("number of docs: " + this.docs.size());
long start = System.currentTimeMillis();
for (Document t : this.docs) {
Document temp = new Document(t);
temp.setTokens(Arrays.asList(t.text.split(this.splitter)));
this.result.add(temp);
}
this.latch.countDown();
System.out.println(String.format("split documents costs: %d ms", System.currentTimeMillis() - start));
}
public List<Document> getResult() {
return result;
}
}
我不确定我能否具体说明您的时间安排,因为仍然不清楚您是如何准确测量/计算数字的,但您是对的,“多线程不会线性加速线程数...... 1/n " 在大多数情况下。 为什么?
我确定您听说过阿姆达尔定律 ( https://en.wikipedia.org/wiki/Amdahl%27s_law )。 只有当您没有程序的任何顺序部分,即无法并行化的程序部分时,您才能实现“1/n”。 但是,即使您自己的代码/业务逻辑/算法(或其要并行化的部分)没有这样的部分,也有执行您的代码的底层软件和硬件层,并且这些层可能会因为争用而引入一些顺序部分一些资源。 例如,您一次只能通过单模光纤或单根双绞线传递一个以太网数据包; 您的硬盘一次只能在其磁头的一个 position 中写入数据; 由于前端总线和 memory 总线,您可以顺序访问主 memory; L2/L3 缓存失效需要做额外的工作才能同步; 转换后备缓冲区 (TLB) 未命中导致走到主 memory(有关前端总线和 memory 总线的信息,请参见上文); 在堆中的 JVM 中分配一个新的 object(当线程本地分配缓冲区(TLAB)已满时)需要一些内部同步; 您可以在压缩时在 GC 上暂停,这看起来像是代码的顺序部分; 类加载和 JIT 的活动将它们的顺序部分添加到应用程序中; 第 3 方库可能在 static 值上有一些同步; 等等等等
因此,您只能在非常非常简单的情况下看到“1/n”,即您的应用程序代码或底层级别都没有使用任何共享资源。 这是一个例子
public class Test {
public static int NUM_OF_THREADS = 4;
public static int NUM_OF_ITERATIONS = 100_000_000;
static class Job extends Thread {
private final AtomicLong counter = new AtomicLong();
@Override
public void run() {
for (int i = 0; i < NUM_OF_ITERATIONS; i++) {
counter.incrementAndGet();
}
}
}
public static void main(String[] args) throws Exception {
final Job[] jobs = new Job[NUM_OF_THREADS];
for (int i = 0; i < jobs.length; i++) {
jobs[i] = new Job();
}
final long startNanos = System.nanoTime();
for (Job job : jobs) {
job.start();
}
for (Job job : jobs) {
job.join();
}
final long spentNanos = System.nanoTime() - startNanos;
System.out.printf("Time spent (s): %f%n", (spentNanos / 1_000_000_000d));
}
}
每个线程递增它自己的计数器实例。 对于NUM_OF_THREADS
= {1,2,3,4} 我得到相同的执行时间~0.6 sec 。 这意味着我们有“1/n”。 我们增加了计算的次数,但我们没有花更多的时间。 我们不会分配新的 memory,因此我们不会遇到 GC 暂停,因此我们不会在线程之间共享任何数据(一个计数器/volatile long 位于所有者内核的 L1 缓存中)
现在,让我们在线程之间共享一个资源。 现在线程递增一个计数器实例。 这将导致缓存失效和密集的内核间通信:
public class Test {
...
private static final AtomicLong counter = new AtomicLong();
...
static class Job extends Thread {
@Override
public void run() {
for (int i = 0; i < NUM_OF_ITERATIONS; i++) {
counter.incrementAndGet();
}
}
}
...
经过这么小的修改,我们看到性能急剧下降。 4 个线程花费了大约 8 秒来完成所有迭代。 8 秒对 0.6 秒(!)
这是一个说明,仅仅理解诸如 big-O 之类的理论知识是不够的,但理解事物在底层是如何工作的,计算机如何准确地执行我们的代码是非常重要的。 重要的是要有一个性能 model,即使是一个非常简化的。
更新:在您将测量值添加到代码中之后......您的测量值看起来是正确的,以及您如何对输入数据进行分区。 这意味着您确实有一个隐藏的执行顺序部分。 在玩过你的代码之后,我认为你有一个 memory 管理瓶颈,你在你的代码中分配了太多的 memory 这会影响执行。 我到底做了什么...
我拿走了你的代码并添加了一个虚拟文档 class。 然后我尝试配置 JVM 以让所有数据适合 Eden/Young 一代,并带有以下选项: -Xmx8G -Xms8G -XX:NewRatio=10 -XX:+PrintGCDetails
numThreads=1 的运行时间约为 1 秒,numThreads=4 的运行时间约为 0.2x-0.3x 秒。 该比率为~1/4-1/3 。 这是意料之中的。 GC 详细信息如下所示:
Heap
PSYoungGen total 2446848K, used 1803997K [0x0000000715580000, 0x00000007c0000000, 0x00000007c0000000)
eden space 2097664K, 86% used [0x0000000715580000,0x0000000783737448,0x0000000795600000)
from space 349184K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007c0000000)
to space 349184K, 0% used [0x0000000795600000,0x0000000795600000,0x00000007aab00000)
ParOldGen total 5592576K, used 0K [0x00000005c0000000, 0x0000000715580000, 0x0000000715580000)
object space 5592576K, 0% used [0x00000005c0000000,0x00000005c0000000,0x0000000715580000)
Metaspace used 2930K, capacity 4494K, committed 4864K, reserved 1056768K
class space used 299K, capacity 386K, committed 512K, reserved 1048576K
您会看到 Eden 没有满,也没有发生 Full GC。
然后我将文档数从 50_000 更改为 100_000 ... numThreads=1 的运行时间约为 2 秒,numThreads=4 的运行时间约为 1 秒。 该比率为~1/2 。 发生了什么?... GC 详细信息包含以下新消息:
[GC (Allocation Failure) [PSYoungGen: 2097664K->349175K(2446848K)] 2097664K->1627175K(8039424K), 0,5229844 secs] [Times: user=4,78 sys=0,45, real=0,53 secs]
这意味着 JVM 无法在 Eden 中分配新的 object 并且必须触发垃圾收集过程。 我们在 GC 中花费了“real=0,53 secs”。
如果您在代码中分配更多的 memory 或具有更小的堆大小,则由于触发了 Full GC,您会得到更糟糕的结果(我给出的 1 线程与 4 线程的比率约为 1/1.4 ):
[Full GC (Ergonomics) [PSYoungGen: 349172K->0K(2446848K)] [ParOldGen: 4675608K->4925474K(5592576K)] 5024780K->4925474K(8039424K), [Metaspace: 2746K->2746K(1056768K)], 5,7606117 secs] [Times: user=56,58 sys=0,26, real=5,76 secs]
这就是为什么我们(在金融科技领域)更喜欢在 Java 中编写分配/无 GC 代码:)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.