for循环可防止上下文切换

时间:2017-05-13 11:36:37

标签: java

随后的简单Java程序表现得很奇怪。启动两个线程后,它们都先完成工作。在同一时间之后,似乎第一个线程不再被抢占,因为第二个线程停止打印出跟踪消息。第一个线程完成后,第二个线程恢复其工作。

当使用for循环的第二个版本 - i在循环开始时递增时,程序的行为与预期一致。 当我在Windows(Windows 10)和Linux(Ubuntu)下执行程序时,我观察到这种行为。我使用Java 8编译器和Java 8运行时。

package test;

import java.time.LocalDateTime;
import java.time.LocalTime;

public class NoContextSwitch {

  private static volatile boolean stopT2 = false;

  public static void main(String[] args) throws InterruptedException {

    Thread t1 = new Thread(() -> {
      System.out.printf("%s (%s): starting calculation", LocalTime.now(),
                        Thread.currentThread().getName());    
      long sum = 0;
      for (int i = 0; i < 1000000; i++) {   // version 1 of loop
      // for (int i = 0; i++ < 1000000; ) { // version 2 of loop
        for (int j = 0; j < 300000; j++)
          sum += 1;
      }      
      System.out.printf("%s (%s): finished calculation: sum=%d%n",
                        Thread.currentThread().getName(), LocalTime.now(), sum);    
    });

    Thread t2 = new Thread(() -> {
      for (int i = 1; i <= 1000000; i++) {
        System.out.printf("%s (%s): i=%d%n", Thread.currentThread().getName(), 
                          LocalTime.now(), i);
        if (stopT2)
          return;
      }
    });

    t1.start();
    t2.start();

    t1.join();
    stopT2 = true;
  }
}

输出:

...
Thread-1 (13:24:23.617): i=25362
Thread-1 (13:24:23.617): i=25363
Thread-1 (13:24:23.617): i=25364
Thread-1 (13:24:23.617): i=25365
Thread-1 (13:24:23.617): i=25366
Thread-1 (13:24:23.617): i=25367
Thread-1 (13:24:23.617): i=25368
Thread-1 (13:24:23.617): i=25369
Thread-1 (13:24:23.617): i=25370
Thread-1 (13:24:23.617): i=25371
Thread-1 (13:24:23.617): i=25372
Thread-1 (13:24:23.617): i=25373
Thread-1 (13:24:23.617): i=25374
Thread-1 (13:24:23.617): i=25375 // Thread-1 gets no time slice for 6 seconds
Thread-1 (13:24:29.646): i=25376
Thread-0 (13:24:29.646): finished calculation: sum=300000000000
Thread-1 (13:24:29.646): i=25377

编辑:程序行为摘要

在做了一些进一步的实验后,我对程序行为的看法如下:

  • 在开始时(直到大约80000遍历外部循环)两个线程同时执行。
  • 然后会发生导致运行时将CPU专门分配给第一个线程的事情。只要执行此线程,就不会调度其他线程(即使不是主线程)。程序行为发生这种变化的原因可能是热点编译器变为活动状态(参见下面 lorenzo 的综合答案)。
  • 当第一个线程完成时,所有其他线程再次获得控制权。
  • 更改线程优先级无效。
  • 如果将Thread.sleep(0)Thread.yield()或print语句插入到第一个线程的循环中,程序将按预期运行(所有线程从CPU时间获取其共享)。即使访问volatile变量也足够了。

1 个答案:

答案 0 :(得分:1)

起初我虽然即使没有大量的分配正在进行,但GC仍在进行中。 所以我以这种方式运行程序:

java -verbose:gc test.NoContextSwitch

实际上,在长时间停顿之前就有GC活动,但运行它的堆更多:

java -Xms1024m -Xmx1024m -verbose:gc test.NoContextSwitch

没有让延迟消失。

所以我的第二个猜测是HotSpot优化器。在正常的程序执行期间,代码被分析,并且当优化器检测到代码的“热点”部分时,这将在运行中进行优化。好吧,几乎在飞行中,需要一点时间。 这是检查此命令的命令行:

java -XX:+PrintCompilation -XX:+CITime test.NoContextSwitch

(有关详细信息,请参阅here),以便您可以看到HotSpot正在完成其工作:

java.util.Formatter$FormatSpecifier::printString (105 bytes)   made not entrant
   8507  298 %     4       test.NoContextSwitch::lambda$main$0 @ -2 (98 bytes)   made not entrant

我无法告诉你所执行的确切类型,但一般来说,匿名类,lambdas等比标准代码慢,并且是优化的常见目标(至少在前一段时间这是真的。 ..)。老实说,这么小的课程花了很多时间

出于好奇,我试图提取两个Thread子类来分离顶级类,我得到了相同的结果。

作为旁注:我的第一个想法是在工作线程中添加一个Thread.yield以查看它是否有任何区别。当你有一个非常紧凑的循环时,这是需要考虑的事情。如果该命令对现代JVM执行该操作有什么影响,那么就可以对每种情况进行评估/测量。

迭代次数如何以及为何影响行为

我在工作线程的外部迭代中用不同的数字进行了一些测试:

0.1mil

Thread-1 (17:30:38.704): i=12936
Thread-1 (17:30:38.704   1856  281 %     4       test.NoContextSwitch::lambda$main$0 @ -2 (98 bytes)   made not entrant
): i=12937
Thread-1 (17:30:39.534): i=12938
Thread-0 (17:30:39.533): finished calculation: sum=   1856  572       4       java.lang.Long::getChars (221 bytes)   made not entrant
30000000000
Thread-1 (17:30:39.534): i=12939
   1857  571       4       java.util.Formatter::parse (151 bytes)   made not entrant

0.5密耳

Thread-1 (17:13:02.380): i=30139
Thread-1 (17:13:02.380): i=30140
Thread-1   4215  299 %     4       ( test.NoContextSwitch::lambda$main$0 @ -2 (98 bytes)   made not entrant
17:13:05.687): i=30141
Thread-1 (17:13:05.687): i=30142
Thread-1 (17:13:05.687): i=30143

1mil(原始值)

Thread-1 (17:20:47.435): i=30010
Thread-1 (17:20:47.435): i=30011
Thread-1 (17:20:55.851): i=30012
Thread-1 (17:20:55.851): i=   9324 30013 286 
%     4      Thread-1 test.NoContextSwitch:: (lambda$main$017:20:55.851 @ -2): i= (98 bytes)   made not entrant30014
Thread-1 (17:20:55.851): i=30015

2密耳

Thread-1 (17:20:03.778): i=25926
Thread-1 (17:20:03.778): i=   1011  486   !   3       java.util.Formatter::format (271 bytes)   made not entrant
25927
Thread-1 (  24471  565       4       java.util.Formatter$FormatSpecifier::print (243 bytes)   made not entrant
  24471  288 %     4       test.NoContextSwitch::lambda$main$0 @ -2 (98 bytes)   made not entrant
17:20:27.250): i=25928
Thread-0 (  24471  577       4       java.util.Formatter$FormatSpecifier::printString (105 bytes)   made not entrant
17:20:27.250): finished calculation: sum=  24472  603       4       java.util.Formatter$FormatSpecifier::print (463 bytes)   made not entrant
600000000000
Thread-1 (17:20:27.250): i=25929
Thread-1 (17:20:27.250): i=25930
Thread-1 (17:20:27.251): i=25931
  24472  581       4       java.util.Formatter::parse (151 bytes)   made not entrant

3mil的

Thread-1 (17:19:10.247): i=12161
Thread-1 (17:19:40.630): i=12162
Thread-1  31405  ( 594 17:19:40.630      ): i=3      12163 java.lang.ClassLoader::
checkName (43 bytes)
Thread-1 (17:19:40.630  31405 ): i= 293 12164%     
4       test.NoContextSwitch::lambda$main$0Thread-1 @ -2 ( (98 bytes)17:19:40.630   made not entrant): i=
12165
Thread-1 (17:19:40.630): i=12166
Thread-1 (17:19:40.630): i=12167
Thread-0 (17:19:40.630): finished calculation: sum=  31405  584       4       java.lang.Long::getChars (221 bytes)   made not entrant
900000000000
Thread-1 (17:19:40.630): i=12168
  31405  585       4       java.util.Formatter::parse (151 bytes)   made not entrant

4密耳

Thread-1 (17:16:56.893): i=11209
Thread-1  40277  284  (%     4      17:17:36.150 test.NoContextSwitch::): i=lambda$main$0 @ -2 (98 bytes)   made not entrant11210

Thread-1 (17:17:36.150): i=11211
Thread-1 (17:17:36.150): i=11212
Thread-1 (17:17:36.150): i=11213
Thread-1 (17:17:36.150): i=11214
Thread-1 (17:17:36.150): i=11215
Thread-0 (17:17:36.150): finished calculation: sum=  40278  585       4       java.lang.Long::getChars (221 bytes)   made not entrant
1200000000000
Thread-1 (17:17:36.150): i=11216
  40278  584       4       java.util.Formatter::parse (151 bytes)   made not entrant
Done  40278  456       3       java.util.Formatter$FormatSpecifier::printString (105 bytes)   made not entrant
  40278  601       4       java.io.PrintStream::printf (7 bytes)

所以是的,暂停持续时间“取决于”迭代次数。我可以推测一些事情:

  • 数字越大,延迟时间越长,程序会在大暂停结束后立即终止(在上面的输出中查找“完成计算”字符串)。我的直觉是工作线程正在减慢整个大优化步骤的优化器,这使得暂停时间更长。使用1mil值,优化器启动并开始工作,但同时工作线程结束,优化器更快地完成工作。 这部分反映在数据中:1大时后的“大停顿”接近20/30秒并以某种方式稳定下来。大约1 / 2mil我认为我们有分水岭。 这意味着优化器在另一个线程仍在运行时阻塞“打印机线程”。也许是因为它只是优化了这个lambda(lambda $ main $ 1 vs lambda $ main $ 0)。

  • 如果数字很短,则不会触发大的优化步骤,因此只有很小的暂停。换句话说,我们可能不会考虑相同的优化,或者我们可能会有相同的更温和的版本。 例如,“test.NoContextSwitch :: lambda $ main $ 0”优化行多次比较。

  • 最后,更改循环“结构”可能会改变优化程序看到它的方式。

请注意,“打印机线程”编号不是一个很好的参考,他们欺骗了我几次,因为它们完全独立于其他任何东西(多次运行相同的代码会将大停顿放在不同的“位置”)

(*)我做了几个实验写入文件而不是控制台,但没有太多。