随后的简单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
在做了一些进一步的实验后,我对程序行为的看法如下:
Thread.sleep(0)
,Thread.yield()
或print语句插入到第一个线程的循环中,程序将按预期运行(所有线程从CPU时间获取其共享)。即使访问volatile变量也足够了。答案 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”优化行多次比较。
最后,更改循环“结构”可能会改变优化程序看到它的方式。
请注意,“打印机线程”编号不是一个很好的参考,他们欺骗了我几次,因为它们完全独立于其他任何东西(多次运行相同的代码会将大停顿放在不同的“位置”)
(*)我做了几个实验写入文件而不是控制台,但没有太多。