为什么ScheduledExecutorService.shutdown()会占用100%的CPU?

9

我有以下简单代码:

package main;

import java.util.concurrent.*;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        new Main();
    }

    public Main() throws InterruptedException {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.schedule(new MyRunnable(), 10, TimeUnit.SECONDS);
        System.out.println("Shutting down...");
        executor.shutdown();
        System.out.println("Awaiting termination...");
        executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MINUTES);
        System.out.println("Main finished!");
    }

    private class MyRunnable implements Runnable {
        public void run() {
            System.out.println("Finished running!");
        }
    }

}

实际上,虽然我的真实代码比这个复杂一些,但我已经在这些行中找到了问题所在。该代码等待10秒钟来运行可运行对象,然后通知主程序的结束。

然而,我注意到在10秒的时间段内,我的一个核心被占用了100%。

如果我注释掉这行:

executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MINUTES);

中央处理器核心也被使用了100%,并且主程序在Runnable之前完成。

如果我注释掉这一行:

executor.shutdown();

CPU使用正常但程序无法完成。

如果我将前两行代码注释掉,则CPU使用正常但主程序无法完成。

  1. 我的代码有问题吗?
  2. executor.shutdown();是否在进行繁忙等待而非仅禁用提交新任务?
  3. 还是应该归咎于JVM?

附加细节:

$ java -version
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) Server VM (build 20.1-b02, mixed mode)

$ uname -a
Linux XPSG 2.6.32-5-686-bigmem #1 SMP Sun May 6 04:39:05 UTC 2012 i686 GNU/Linux

PS: 请不要让我使用CountDownLatch或者newSingleThreadScheduledExecutor。这和我提问的问题无关。谢谢。

编辑:

这是Java转储:

Full thread dump Java HotSpot(TM) Server VM (20.1-b02 mixed mode):

"pool-1-thread-1" prio=10 tid=0x08780c00 nid=0x32ee runnable [0x6fdcc000]
   java.lang.Thread.State: RUNNABLE
    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:943)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:907)
    at java.lang.Thread.run(Thread.java:662)

"Low Memory Detector" daemon prio=10 tid=0x0874dc00 nid=0x32ec runnable [0x00000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" daemon prio=10 tid=0x0874c000 nid=0x32eb waiting on condition [0x00000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" daemon prio=10 tid=0x0874a000 nid=0x32ea waiting on condition [0x00000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" daemon prio=10 tid=0x08748800 nid=0x32e9 waiting on condition [0x00000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" daemon prio=10 tid=0x0873a000 nid=0x32e8 in Object.wait() [0x70360000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x9e8f1150> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:118)
    - locked <0x9e8f1150> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:134)
    at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)

"Reference Handler" daemon prio=10 tid=0x08735400 nid=0x32e7 in Object.wait() [0x703b1000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x9e8f1050> (a java.lang.ref.Reference$Lock)
    at java.lang.Object.wait(Object.java:485)
    at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)
    - locked <0x9e8f1050> (a java.lang.ref.Reference$Lock)

"main" prio=10 tid=0x086b5c00 nid=0x32e3 waiting on condition [0xb6927000]
   java.lang.Thread.State: TIMED_WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x9e958998> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:198)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2025)
    at java.util.concurrent.ThreadPoolExecutor.awaitTermination(ThreadPoolExecutor.java:1253)
    at main.Main.<init>(Main.java:19)
    at main.Main.main(Main.java:10)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.eclipse.jdt.internal.jarinjarloader.JarRsrcLoader.main(JarRsrcLoader.java:58)

"VM Thread" prio=10 tid=0x08731800 nid=0x32e6 runnable 

"GC task thread#0 (ParallelGC)" prio=10 tid=0x086bd000 nid=0x32e4 runnable 

"GC task thread#1 (ParallelGC)" prio=10 tid=0x086be400 nid=0x32e5 runnable 

"VM Periodic Task Thread" prio=10 tid=0x0874fc00 nid=0x32ed waiting on condition 

JNI global references: 931

Heap
 PSYoungGen      total 18752K, used 645K [0x9e8f0000, 0x9fdd0000, 0xb3790000)
  eden space 16128K, 4% used [0x9e8f0000,0x9e991510,0x9f8b0000)
  from space 2624K, 0% used [0x9fb40000,0x9fb40000,0x9fdd0000)
  to   space 2624K, 0% used [0x9f8b0000,0x9f8b0000,0x9fb40000)
 PSOldGen        total 42880K, used 0K [0x74b90000, 0x77570000, 0x9e8f0000)
  object space 42880K, 0% used [0x74b90000,0x74b90000,0x77570000)
 PSPermGen       total 16384K, used 2216K [0x70b90000, 0x71b90000, 0x74b90000)
  object space 16384K, 13% used [0x70b90000,0x70dba198,0x71b90000)

当CPU占用率达到100%时,您能否获取线程转储? - John Vint
1
我知道这并不回答你的问题,但是当我使用OpenJDK 1.7.0_03运行你的程序时,我的核心使用率大约为1%。 - Jeffrey
@JohnVint,我刚刚添加了它。 - Mosty Mostacho
@Jeffrey 哦,这很有趣。看起来可能是某种JVM问题。也许JohnVint可以通过线程转储帮助解决这个问题(对我来说看起来像中文 :) - Mosty Mostacho
@MostyMostacho,你确定线程转储在10秒钟之前发生了吗?pool-1-thread-1应该处于TIMED_WAITING状态,等待任务而不是RUNNABLE状态(至少我这样)。 - Jeffrey
@Jeffrey 百分百确定。注意:我改动的唯一一件事是延迟时间(我将其设置为 600,以便让我有足够的时间获取进程的 PID)。顺便说一下,我重新测试了一下,结果相同(pool-1-thread-1 处于“运行中”状态)。 - Mosty Mostacho
2个回答

4
实际上是忙等待。 ThreadPoolExecutor似乎没有后退逻辑来等待所有任务完成 (请注意,只有在您 shutdown() 时才会发生,否则它会正确挂起线程)。
它会不断检查任务是否准备好执行,如果没有,它将重试直到经过了预定时间以使任务可以被调度。
关闭计划的线程池存在一个权衡(这个权衡是由实现强加的)。它会忙于旋转,直到任务准备好进行调度或者使用shutdownNow停止所有队列中的任务。 但是您可以取得返回的Runnable列表并自行执行它们。

1
哇,真不可思议,它竟然使用忙等待实现了。看来我的选择是升级到Java7或重新实现这个轮子。后者似乎更有趣 :) 谢谢John! - Mosty Mostacho
@MostyMostacho 是的,我也有点这样的感觉。它似乎委托给TPE的默认实现,像那样旋转,但不会很忙,因为在普通线程池中没有调度概念。不过很高兴看到Java 7中已经解决了这个问题。 - John Vint

3
这是一个与平台相关的问题。当我在我的机器上运行您的测试程序时,在10秒的关闭期间,根据我的机器的CPU使用情况监测,CPU使用率近似为零。
$ java -version
java version "1.7.0_03"
Java(TM) SE Runtime Environment (build 1.7.0_03-b04)
Java HotSpot(TM) Client VM (build 22.1-b02, mixed mode, sharing)

我对Java错误数据库进行了粗略搜索,没有找到相关内容。从网上(Google)可以找到不同版本的源代码,很明显getTask方法和相关方法在早期的Java 1.6和当前的Java 1.7之间经历了很多工作。建议您尝试将JVM升级到最新的Java 1.6或Java 1.7,或者至少在测试程序中尝试一下。如果不行,也可以放弃更改。这并不是一个重大问题...
顺便说一下,此页面包括有关在Ubuntu上安装各种Java版本的说明。
其中一个选项(适用于Java 7)是使用“duinsoft”安装程序,这是一个从Oracle网站获取安装程序的脚本。他们甚至设置了一个deb存储库来托管安装程序。
另一个选择是安装openjdk-7-jdk或openjdk-7-jre软件包,它们位于11.10 repos中。
当您在该区域时,不要忘记对此RFE进行投票,以提供Java 7的debian软件包/安装程序。 值得一提的是,这个混乱主要是由Oracle撤回OEM重新分配许可证所致,这意味着必须撤回“sun-java-6”软件包。当然,还有一个“小问题”,即Oracle没有提供DEB。

嗨Stephen,感谢您的意见。确实这可能是JVM的某些问题。然而,我宁愿继续使用当前的VM,因为我不想离开Debian稳定的存储库(无论如何,据我所知,Java 7甚至没有进入实验性的存储库)。再次感谢! - Mosty Mostacho
没有任何表情符号可以表达我的惊讶:“Oracle自己将使用OpenJDK作为他们未来版本的基础!” - Mosty Mostacho
1
@MostyMostacho - 没有什么新的。这只是Oracle试图“赚钱”Java的又一个例子。(但如果你需要一个新的表情符号来表达自己……尝试这个……:-{u) - Stephen C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接