当前位置: 首页 > 面试题库 >

在几乎不改变执行顺序的情况下,如何分配变量会导致严重的性能下降?

葛昱
2023-03-14
问题内容

在使用多线程时,我会发现一些与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个DemoThread,然后将其启动并加入。每个循环中唯一的区别是调用“
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”关键字,性能差异也很明显,至少在两台计算机和OS上是我测试了代码。这是一个简短的摘要:

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
OS名称:Mac OS X
OS架构:x86_64
OS版本:10.6.5
处理器/核心:8

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

没有volatile关键字:
最终结果:
26009 ms。实例化后调用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
OS名称:Windows 7
OS Arch:amd64
OS版本:6.1
处理器/核心:4

带有volatile关键字:
最终结果:
18120 ms。实例化后调用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
OS名称:Linux
OS Arch:amd64
OS版本:2.6.32-28-通用
处理器/内核:4

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

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

Mac OS详细信息(易失):

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

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

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

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

(依此类推,有时“慢速”循环中的一个或两个线程会按预期完成,但大多数情况下不会。)

给出的示例从理论上看是没有用的,这里不需要’volatile’-但是,如果您使用’java.util.Random’-Instance而不是’Container’-Class并调用,例如,nextInt()多次,将产生相同的效果:如果在Thread的构造函数中创建对象,则将快速执行该线程,但如果在run()方法中创建该对象,则将缓慢执行该线程。我相信Mac
OS上的Java Random Slowdowns中描述的性能问题一年多以前与这种效果有关,但我不知道为什么会这样-
除此之外,我确定不应该那样做,因为这将意味着创建一个新的除非您知道对象图中不会涉及任何易失变量,否则线程运行方法中的对象都是对象。剖析无济于事,因为这种情况下问题消失了(与Mac
OS上的Java Random Slowdowns相同的观察,续),并且在单核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

在第一种方案中,两个volatile变量几乎不可能彼此接近。在第二种方案中,几乎可以肯定是这种情况。

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

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

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

幸运的是,AB可以直接对新值,而无需通过主内存去沟通。但是,这需要花费额外的时间。

为了验证这一理论,您可以在中填充额外的128个字节Container,以使两个的volatile变量Container不会落在同一高速缓存行中。那么您应该观察到这两个方案执行大约需要相同的时间。

经验教训:通常,CPU假定相邻变量是相关的。如果我们想要自变量,则最好将它们彼此远离。



 类似资料:
  • 本文向大家介绍mysql不重启的情况下修改参数变量,包括了mysql不重启的情况下修改参数变量的使用技巧和注意事项,需要的朋友参考一下 通常来说,更新mysql配置my.cnf需要重启mysql才能生效,但是有些时候mysql在线上,不一定允许你重启,这时候应该怎么办呢? 看一个例子: mysql> show variables like 'log_slave_updates'; +-------

  • 我的程序中只有两个线程。 我读过Java内存模型,从线程2中我所理解的读一个总是给我1。 我想知道我的理解是否正确。 特别是可以重新排序仍然发生,所以我看到a=0在第二个线程?

  • 我正在尝试从组件更新(添加,删除)查询参数。在angularJS中,由于: 我有一个应用程序,其中包含用户可以过滤,排序等列表,我想在url的查询参数中设置所有激活的过滤器,以便他可以复制/粘贴URL或与其他人共享。

  • 虽然我的问题主题似乎是许多PDF操作包和工具都支持的特性,但我需要明确指出,我不想旋转PDF。 我有一个PDF,它显示了一个纵向(A4),尺寸为WxH 297x210(A4旋转)。 现在,我需要实现的是,这个PDF有横向方向,同时保留尺寸。 我不确定这需要我做什么。 如果我使用Adobe Illustrator将文档格式更改到所需的位置,我还需要旋转内容。如果我将此页面放入设计糟糕的PDF中,此页

  • 有没有一种简单的方法可以以灵活的方式向Kubernetes中的几个容器提供环境变量,无论是通过Helm还是另一种工具? 我目前正在考虑使用Kustomize来完成Helm填写模板后的最后一英里更改,但我却一直在设置Kustomize补丁。在我的场景中,环境变量由Helm在ConfigMap中填写。我想添加一个字段来读取ConfigMap并将给定的环境变量添加到容器中。我想通过kustoMize将e

  • 问题内容: 这个问题已经在这里有了答案 : 如何在没有换行符或空格的情况下进行打印? (22个答案) 3年前关闭。 我想知道在打印某些内容时如何删除其他空格。 就像我这样做时: 输出将是: 但是我想要: 有什么办法吗? 问题答案: 如果您不需要空格,请不要使用。使用字符串串联或格式化。 级联: 格式: 后者要灵活得多,请参见方法文档和“ 格式化字符串语法” 部分 。 您还将遇到较早的格式化样式: