【问题标题】:Java: Why is the main-thread not progressing?Java:为什么主线程没有进展?
【发布时间】:2016-03-20 15:20:03
【问题描述】:

我在使用多线程 Java 程序时遇到了一些问题,并将其提炼成一个非常简单的示例——我的困惑仍然不少!

下面显示的示例程序。那么这是做什么(或打算做什么)?嗯,main() 函数从一个简单的线程开始,它基于一个静态的内部类 Runnable。此 Runnable 包含两个嵌套循环,对局部变量“z”进行简单计算,总共 10^12 次迭代 (10^6 * 10^6),之后它将打印出结果并退出。在生成这个工作线程之后,主线程进入它自己的循环,在其中将字符串“Z”打印到控制台,之后它休眠(使用 Thread.sleep())1 秒钟,然后一遍又一遍地重复这个过程。

所以运行这个程序,我希望它在计算线程执行其工作时每 1 秒打印一次“Z”。

然而,实际上发生的是计算线程启动,主线程显示第一个“Z”,然后什么也没发生。它似乎无限期地或至少比请求的 1000 毫秒长得多,挂在 Thread.sleep 调用中。

请注意,这是在具有多线程的快速四核机器上,因此同时运行线程应该没有问题。其他核心在 Windows 任务管理器中显示为空闲。此外,即使在单核系统上,我也希望操作系统定期抢占计算线程,以允许主线程打印字符串。 此外,线程之间没有共享变量或锁,因此它们不应相互阻塞。

更奇怪的是,Runnable 中有两个嵌套循环似乎对行为至关重要。只需一个循环,使用相同的迭代次数,一切都会按预期进行。

我已经在 Windows 10 64 位和 Java 1.8.0_73 上对此进行了测试。

有人可以解释这种行为吗?

public class Calculate {
    static class Calc implements Runnable
    {
        @Override
        public void run() {
            int z = 1;
            for(int i = 0; i < 1000000; i++) {      
                for(int j = 0; j < 1000000; j++) {
                    z = 3*z + 1;
                }               
            }
            System.out.println("Result: " + z);
        }
    }

    public static void main(String[] args)  throws Exception
    {
        Thread t = new Thread(new Calc());
        t.start();
        while(true) {
            System.out.println("Z");
            Thread.sleep(1000);
        }
    }
}

更新 1:有人建议这可能与 Busy loop in other thread delays EDT processing 重复。在我的案例中,许多症状是相同的,但很难判断它们是否真的是相同的原因,因为另一个问题似乎没有被完全诊断出来。但这很有可能。

更新 2:我已向 Oracle 提交了错误报告。如果它被接受并且我找到它,我会发布链接。

更新 3:在 Java 8 和 9 中被接受为错误:https://bugs.openjdk.java.net/browse/JDK-8152267

【问题讨论】:

  • 这里似乎可以正常工作,您可能需要在Calc 的最内层循环中添加Thread.yield。此外,您的 z 最终会溢出,并且您正在循环进行 非常 大量迭代。
  • @Elliott:有趣的是,它按预期工作。为什么 yield() 是必要的,我的意思是有很多程序在后台线程中进行“深度”计算,通常不建议它们必须屈服,而且我实际上从未见过在 Java 中这样做过。确实“z”会溢出,但此计算仅用于测试示例进行一些虚拟计算。对于 int 来说,溢出是完全合法且明确定义的。并且故意增加迭代次数来展示问题。但请注意,CPU 密集型计算需要很长时间是很常见的。
  • 我想我正在寻找的是某种关于“这个程序在后台计算的方式有什么问题”的指南。 IE。如果这是错误的,那么背景计算的其他情况可能是错误的。什么时候你必须让步等等的规则是什么?
  • 您正在测试的机器上有多少个内核?您正在运行多少其他进程? Thread 适合我。
  • @Elliott:这是一个 6700K - 4 个真正的核心(4 GHz)和 8 个逻辑核心,由于超线程。没有其他重要进程正在运行(标准 Windows 和 Eclipse 除外)。当我启动程序时,CPU 使用率为 1%,并且似乎没有任何内核在忙于任何事情。也没有其他系统问题。你用的是哪个操作系统和Java版本?

标签: java multithreading


【解决方案1】:

好像你的程序溢出了我的机器,我也只得到一次 Z(Os x、i7、16G)。

编辑:每次循环 i 时,您可以确保让主线程有机会打印“z”:

@Override
public void run() {
    int z = 1;
    for(int i = 0; i < 1000000; i++) {    
        Thread.yield();
        for(int j = 0; j < 1000000; j++) {
            z = 3*z + 1;
        }               
    }
    System.out.println("Result: " + z);
}

【讨论】:

  • Benoit:不确定我所说的“程序溢出事件的正常和执行顺序”是什么意思。这个程序违反了什么“合同”?主线程应该进展不是一个合理的期望吗? Java 资源或计算资源都没有资源争用。它运行在一个不受限制的系统上。 Windows 任务管理器将系统显示为 15% 忙,仅占用一个内核。
  • 我想这与 Java 实现循环的方式有关,当我减少 2 次打印“z”之间的时间时,它工作正常。一个理论是,如果他是,Java 不会唤醒其他线程做太密集的手术?
【解决方案2】:

正如我在描述中的“更新 3”中所说,这原来是 Java 8 和 9 中的一个错误。所以这是这个问题的正确答案:程序没有问题,行为是由某些 Java 版本中的错误引起的,导致它无法正常运行。虽然在程序中插入暂停、让步等的建议可能与寻求实际解决方法的人相关,但“缺少这些解决方法”并不是问题的根本原因,也不代表原始程序中的错误。

【讨论】:

    【解决方案3】:

    简答

    这是CPU分配的问题。要解决此问题,请在 for (int j=0 ...) 循环之后添加行 Thread.currentThread().yield();

    长答案

    我用下面修改的程序来查找问题

    public class Calculate {
    
        public static final int[] LOOP_SIZES = {
            1000, 5000, 10000, 50000, 65000, 66000, 100000, 500000, 1000000 };
    
        static class Calc implements Runnable
        {
            private int loopSize;
            public Calc(int loopsize) {
                loopSize = loopsize;
            }
            @Override
            public void run() {
                int z = 1;
                final long startMillis = System.currentTimeMillis();
                for(int i = 0; i < loopSize; i++) {
                    for(int j = 0; j < loopSize; j++) {
                        z = 3*z + 1;
                    }
                    Thread.currentThread().yield();  // comment this line to reproduce problem                                         
                }
                final long endMillis = System.currentTimeMillis();
                final double duration = ((double)( endMillis - startMillis )) / 1000.0;
                System.out.println("Result for " + loopSize +
                                   ": " + z +
                                   " @ " + duration + " sec");
            }
        }
    
        public static void main(String[] args)  throws Exception
        {
            Thread tt[] = new Thread[LOOP_SIZES.length];
            for (int i = 0; i < LOOP_SIZES.length; i++) {
                final int ls = LOOP_SIZES[i];
                tt[i] = new Thread(new Calc(ls));
                tt[i].start();
            }
            int nbNonTerminated = 1;
            while(nbNonTerminated > 0) {
                nbNonTerminated = 0;
                for (int j=0; j < tt.length; j++)  {
                    printThreadState( tt[j], "Thread " + j );
                    if (!tt[j].getState().equals(java.lang.Thread.State.TERMINATED))
                        nbNonTerminated ++;
                }
                printThreadState( Thread.currentThread(), "Main thread");
                System.out.println("Z @ " + System.currentTimeMillis());
                Thread.sleep(1000);
            }
        }
    
        public static void printThreadState( Thread t, String title ){
            System.out.println( title + ": " + t.getState() );
        }
    }
    

    如果你运行这个程序,主线程会按预期工作(它每秒打印一条消息)。如果您用yield 注释该行,重新编译并运行它,主循环会打印一些消息,然后阻塞。似乎 JVM 几乎将所有 CPU 分配给其他线程,导致主线程无响应。

    线程数似乎没有改变问题。当“循环大小”变得大于大约 65000 时,问题开始出现。另外,请注意,当线程完成时我没有加入线程。

    注意:在 Mac OSX、2.7.5、2.5 GHz Core i5 上测试。
    我还在 Raspberry Pi 2、Raspbian linux 4.1.3(功能要弱得多的机器)上对其进行了测试:没有显示问题。

    【讨论】:

    • Java 现在不是在使用本机操作系统线程吗?如果是这样,调度应该由os完成。在 windows 主线程曾经是特殊的(默认处理窗口事件),如果这没有改变,它可能是问题的一部分:例如,窗口渲染/刷新事件正在排队并且线程永远不会退出睡眠
    • OP 说他在 Windows 10 中遇到了问题,我能够在 OSX 中重现它。我对JVM内部的了解不多。但是,当 loop_size 约为 2^16 时出现问题似乎很奇怪(所以两个循环都约为 2^32--巧合?)。似乎当一个线程成功地获取了大量 CPU 周期时,JVM 会在某个时候为其分配几乎所有资源。 “你拥有的越多,你想要的就越多”或“你花的越多,他们给你的就越多”。这可以解释为什么问题不会出现在功能较弱的机器上。
    • 我会尝试将等待循环移到主线程之外,以确保主线程没有做意外的事情(osx 是窗口系统,需要处理事件 afaik)
    • 您好,谢谢,我同意这可以解决问题(事实上,只需在外循环中调用一次 yield 就足够了)。但我不认为这解释了这种行为,这不是线程应该如何工作的。有多少程序启动后台线程进行计算(可能需要几分钟)确保定期调用 yield?如果是这样的话,这显然是一个 JVM 错误,因为没有说明何时需要这样做,本质上是让事情发生。 @Sami:与 Windows 无关,Java 主线程!= Windows 程序主线程。
    【解决方案4】:

    在 run 方法中你没有循环,只是一个简单的 z 计算。 因此,当您调用 t.start(); run 方法被调用一次,计算 z,然后你就让线程永远休眠。

    【讨论】:

    • 不确定我是否关注。 run() 中有一个循环,您是否暗示它以某种方式被优化掉了?情况似乎并非如此,因为它显示一个核心在运行时完全忙碌。此外,即使是这种情况,程序仍应继续永远显示“Z”。
    猜你喜欢
    • 2019-12-21
    • 2013-08-12
    • 1970-01-01
    • 2017-11-28
    • 1970-01-01
    • 2017-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多