如果执行顺序(几乎)不受影响,如何在严重的性能下降中分配变量结果?

在玩multithreading时,我可以观察到与AtomicLong(以及使用它的类,例如java.util.Random)相关的一些意外但严重的性能问题,我目前没有解释。 但是,我创建了一个简约示例,它基本上由两个类组成:一个类“Container”,它保存对volatile变量的引用;一个类“DemoThread”,它在线程执行期间对“Container”的实例进行操作。 请注意,对“Container”和volatile long的引用是私有的,并且从不在线程之间共享(我知道这里不需要使用volatile,它仅用于演示目的) – 因此,“DemoThread”的多个实例应该完美运行在多处理器机器上并行,但由于某种原因,它们没有(完整的例子在这篇文章的底部)。

private static class Container { private volatile long value; public long getValue() { return value; } public final void set(long newValue) { value = newValue; } } private static class DemoThread extends Thread { private Container variable; public void prepare() { this.variable = new Container(); } public void run() { for(int j = 0; j < 10000000; j++) { variable.set(variable.getValue() + System.nanoTime()); } } } 

在我的测试中,我反复创建了4个DemoThreads,然后启动并加入。 每个循环的唯一区别是“prepare()”被调用的时间(这显然是线程运行所必需的,否则会导致NullPointerException):

 DemoThread[] threads = new DemoThread[numberOfThreads]; for(int j = 0; j < 100; j++) { boolean prepareAfterConstructor = j % 2 == 0; for(int i = 0; i < threads.length; i++) { threads[i] = new DemoThread(); if(prepareAfterConstructor) threads[i].prepare(); } for(int i = 0; i < threads.length; i++) { if(!prepareAfterConstructor) threads[i].prepare(); threads[i].start(); } joinThreads(threads); } 

由于某种原因,如果在启动线程之前立即执行prepare(),则需要两倍的时间才能完成,即使没有“volatile”关键字,性能差异也很大,至少在两台机器和操作系统上我测试了代码。 这是一个简短的总结:


Mac OS摘要:

Java版本:1.6.0_24
Java类版本:50.0
VM供应商:Sun Microsystems Inc.
VM版本:19.1-b02-334
VM名称:Java HotSpot(TM)64位服务器VM
操作系统名称:Mac OS X.
OS Arch:x86_64
操作系统版本:10.6.5
处理器/核心:8

使用volatile关键字:
最终结果:
31979毫秒 当实例化后调用prepare()时。
96482毫秒 在执行之前调用prepare()时。

没有volatile关键字:
最终结果:
26009毫秒 当实例化后调用prepare()时。
35196毫秒 在执行之前调用prepare()时。


Windows摘要:

Java版本:1.6.0_24
Java类版本:50.0
VM供应商:Sun Microsystems Inc.
VM版本:19.1-b02
VM名称:Java HotSpot(TM)64位服务器VM
操作系统名称:Windows 7
OS Arch:amd64
操作系统版本:6.1
处理器/核心:4

使用volatile关键字:
最终结果:
18120毫秒 当实例化后调用prepare()时。
36089毫秒 在执行之前调用prepare()时。

没有volatile关键字:
最终结果:
10115毫秒 当实例化后调用prepare()时。
10039毫秒。 在执行之前调用prepare()时。


Linux摘要:

Java版本:1.6.0_20
Java类版本:50.0
VM供应商:Sun Microsystems Inc.
VM版本:19.0-b09
VM名称:OpenJDK 64位服务器VM
操作系统名称:Linux
OS Arch:amd64
操作系统版本:2.6.32-28-通用
处理器/核心:4

使用volatile关键字:
最终结果:
45848毫秒 当实例化后调用prepare()时。
110754毫秒 在执行之前调用prepare()时。

没有volatile关键字:
最终结果:
37862毫秒 当实例化后调用prepare()时。
39357毫秒 在执行之前调用prepare()时。


Mac OS详细信息(易失性):

测试1,4个线程,在创建循环中设置变量
线程-2在653毫秒后完成。
线程-3在653毫秒后完成。
线程-4在653毫秒后完成。
线程-5在653毫秒后完成。
总时间:654毫秒。

测试2,4线程,在启动循环中设置变量
线程-7在1588 ms后完成。
线程-6在1589 ms后完成。
线程-8在1593 ms后完成。
线程9在1593 ms后完成。
总时间:1594毫秒。

测试3,4个线程,在创建循环中设置变量
线程10在648 ms后完成。
线程-12在648 ms后完成。
线程-13在648 ms后完成。
线程-11在648 ms后完成。
总时间:648毫秒。

测试4个线程,在启动循环中设置变量
Thread-17在1353 ms后完成。
Thread-16在1957 ms之后完成。
线程14在2170毫秒后完成。
线程-15在2169 ms后完成。
总时间:2172毫秒。

(等等,有时’慢’循环中的一个或两个线程按预期完成,但大多数情况下它们没有完成)。

给定的例子在理论上看起来是合理的,因为它没有用,而且这里不需要’volatile’ – 但是,如果你使用’java.util.Random’-Instance而不是’Container’-Class并调用,例如,nextInt()多次,会发生相同的效果:如果在Thread的构造函数中创建对象,线程将快速执行,但如果在run() – 方法中创建它,则会很慢。 我相信一年多以前在Mac OS上Java随机减速中描述的性能问题与这种效果有关,但我不知道它为什么会这样 – 除此之外我确定它不应该像因为这意味着在线程的run方法中创建一个新对象总是很危险的,除非你知道在对象图中不会涉及任何volatile变量。 分析没有帮助,因为在这种情况下问题消失了(与Mac OS续约中的Java随机减速一样 ),并且它也不会发生在单核PC上 – 所以我猜它是一种线程同步问题…然而,奇怪的是,实际上没有什么可以同步,因为所有变量都是线程本地的。

真的很期待任何提示 – 如果您想确认或伪造问题,请参阅下面的测试用例。

谢谢,

斯蒂芬

 public class UnexpectedPerformanceIssue { private static class Container { // Remove the volatile keyword, and the problem disappears (on windows) // or gets smaller (on mac os) private volatile long value; public long getValue() { return value; } public final void set(long newValue) { value = newValue; } } private static class DemoThread extends Thread { private Container variable; public void prepare() { this.variable = new Container(); } @Override public void run() { long start = System.nanoTime(); for(int j = 0; j < 10000000; j++) { variable.set(variable.getValue() + System.nanoTime()); } long end = System.nanoTime(); System.out.println(this.getName() + " completed after " + ((end - start)/1000000) + " ms."); } } public static void main(String[] args) { System.out.println("Java Version: " + System.getProperty("java.version")); System.out.println("Java Class Version: " + System.getProperty("java.class.version")); System.out.println("VM Vendor: " + System.getProperty("java.vm.specification.vendor")); System.out.println("VM Version: " + System.getProperty("java.vm.version")); System.out.println("VM Name: " + System.getProperty("java.vm.name")); System.out.println("OS Name: " + System.getProperty("os.name")); System.out.println("OS Arch: " + System.getProperty("os.arch")); System.out.println("OS Version: " + System.getProperty("os.version")); System.out.println("Processors/Cores: " + Runtime.getRuntime().availableProcessors()); System.out.println(); int numberOfThreads = 4; System.out.println("\nReference Test (single thread):"); DemoThread t = new DemoThread(); t.prepare(); t.run(); DemoThread[] threads = new DemoThread[numberOfThreads]; long createTime = 0, startTime = 0; for(int j = 0; j < 100; j++) { boolean prepareAfterConstructor = j % 2 == 0; long overallStart = System.nanoTime(); if(prepareAfterConstructor) { System.out.println("\nTest " + (j+1) + ", " + numberOfThreads + " threads, setting variable in creation loop"); } else { System.out.println("\nTest " + (j+1) + ", " + numberOfThreads + " threads, setting variable in start loop"); } for(int i = 0; i < threads.length; i++) { threads[i] = new DemoThread(); // Either call DemoThread.prepare() here (in odd loops)... if(prepareAfterConstructor) threads[i].prepare(); } for(int i = 0; i < threads.length; i++) { // or here (in even loops). Should make no difference, but does! if(!prepareAfterConstructor) threads[i].prepare(); threads[i].start(); } joinThreads(threads); long overallEnd = System.nanoTime(); long overallTime = (overallEnd - overallStart); if(prepareAfterConstructor) { createTime += overallTime; } else { startTime += overallTime; } System.out.println("Overall time: " + (overallTime)/1000000 + " ms."); } System.out.println("Final results:"); System.out.println(createTime/1000000 + " ms. when prepare() was called after instantiation."); System.out.println(startTime/1000000 + " ms. when prepare() was called before execution."); } private static void joinThreads(Thread[] threads) { for(int i = 0; i < threads.length; i++) { try { threads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } } } 

}

两个易失性变量ab彼此太靠近,它们落在同一个高速缓存行中; 虽然CPU A只读/写变量a ,而CPU B只读/写变量b ,但它们仍然通过相同的缓存线相互耦合。 这些问题被称为虚假共享

在您的示例中,我们有两个分配方案:

 new Thread new Thread new Container vs new Thread new Thread .... new Container new Container .... new Container 

在第一种方案中,两个易变量变量彼此接近的可能性很小。 在第二种方案中,几乎可以肯定。

CPU缓存不适用于单个单词; 相反,他们处理缓存行。 高速缓存行是连续的内存块,比如64个相邻字节。 通常这很好 – 如果CPU访问一个单元,它很可能也会访问相邻的单元。 除了您的示例,该假设不仅无效,而且有害。

假设ab落在同一缓存行L 。 当CPU A更新a ,它会通知其他CPU L是脏的。 由于B也会缓存L ,因为它在b上工作, B必须删除其缓存的L 因此,下次B需要读取b ,必须重新加载L ,这是昂贵的。

如果B必须访问主内存以重新加载,这是非常昂贵的,它通常慢100倍。

幸运的是, AB可以直接通信新值而无需通过主存。 然而,它需要额外的时间。

为了validation这个理论,你可以在Container填充额外的128个字节,这样两个Container两个volatile变量就不会落在同一个缓存行中; 那么你应该注意到这两个方案大致需要执行相同的时间。

学到的经验:通常CPU假设adjecent变量是相关的。 如果我们想要自变量,我们最好将它们彼此远离。

好吧,你正在写一个volatile变量,所以我怀疑这会强制内存障碍 – 撤消一些可以实现的优化。 JVM不知道在另一个线程上不会观察到该特定字段。

编辑:如上所述,基准测试本身存在问题,例如计时器运行时的打印。 此外,在开始计时之前“预热”JIT通常是一个好主意 – 否则你要测量的时间在正常的长时间运行过程中并不重要。

我不是Java内部的专家,但我读了你的问题并发现它很吸引人。 如果我不得不猜测,我想你发现了什么:

  1. 与volitale属性的实例化没有任何关系。 但是,根据您的数据,属性实例化会影响读取/写入属性的成本。

  2. 与在运行时查找volitale属性的引用有关。 也就是说,我有兴趣看看延迟如何随着更频繁循环的线程而增长。 对volitale属性的调用次数是导致延迟,添加本身还是写入新值的原因。

我不得不猜测:可能有一个JVM优化试图快速实例化该属性,后来,如果有时间,改变内存中的属性,以便更容易读/写它。 也许有一个(1)快速创建的volitale属性读/写队列,以及(2)难以创建但快速调用队列,JVM以(1)开头,后来改变了volitale属性至(2)。

也许如果你在调用run()方法之前就准备好了(),那么JVM没有足够的空闲周期来优化(1)到(2)。

测试这个答案的方法是:

prepare(),sleep(),run()并查看是否得到相同的延迟。 如果睡眠是导致优化的唯一因素,则可能意味着JVM需要循环来优化volitale属性

或者(风险更大)……

prepare()和run()线程,稍后在循环中间,暂停()或sleep()或以某种方式停止对属性的访问,以便JVM可以尝试将其移动到(2)。

我有兴趣看看你发现了什么。 有趣的问题。

嗯,我看到的最大区别在于分配对象的顺序。 在构造函数之后进行准备时,Container分配与Thread分配交错。 在执行之前准备时,首先分配您的线程,然后分配您的容器。

我不太了解多处理器环境中的内存问题,但如果我不得不猜测,可能在第二种情况下,容器分配更可能在同一内存页面中分配,并且可能处理器速度变慢由于争用同一个内存页面而失败?

[编辑]按照这种思路,我有兴趣看看如果你没有尝试回写变量,只能在线程的run方法中读取它会发生什么。 我希望时间差异消失。

[edit2]看到无可争议的答案; 他解释得比我好多了