因此,在接受一些求职面试之后,我想编写一个小程序来检查i++
在java中是否真的是非原子的,并且实际上应该添加一些锁定来保护它。原来你应该,但这不是问题。
所以我在这里编写这个程序只是为了检查它。
事情是,它挂了。似乎主线程卡在t1.join()
上
line,即使两个工作线程都应该因为前一行的stop = true
而完成。
我发现悬挂在下列情况下停止:
boolean stop
标记为volatile
,则立即导致写入
被工人线程看到,或t
标记为volatile
...为此,我不知道是什么原因引起了不挂。有人可以解释一下发生了什么吗?为什么我会看到悬挂,为什么它会在这三种情况下停止?
public class Test {
static /* volatile */ long t = 0;
static long[] counters = new long[2];
static /* volatile */ boolean stop = false;
static Object o = new Object();
public static void main(String[] args)
{
Thread t1 = createThread(0);
Thread t2 = createThread(1);
t1.start();
t2.start();
Thread.sleep(1000);
stop = true;
t1.join();
t2.join();
System.out.println("counter : " + t + " counters : " + counters[0] + ", " + counters[1] + " (sum : " + (counters[0] + counters[1]) + ")");
}
private static Thread createThread(final int i)
{
Thread thread = new Thread() {
public void run() {
while (!stop)
{
// synchronized (o) {
t++;
// }
// if (counters[i] % 1000000 == 0)
// {
// System.out.println(i + ")" + counters[i]);
// }
counters[i]++;
}
};
};
return thread;
}
}
答案 0 :(得分:8)
似乎主线程卡在
t1.join()
行上,即使两个工作线程都应该因为前一行的stop = true
而完成。
在没有volatile
,锁定或其他安全发布机制的情况下,JVM没有义务永远使stop = true
对其他线程可见。特别适用于您的情况,当您的主线程休眠一秒钟时,JIT编译器将您的while (!stop)
热循环优化为等效的
if (!stop) {
while (true) {
...
}
}
这种特殊的优化被称为"吊装"循环中的读取动作。
我发现悬挂在下列情况下停止:
- 我在工作线程中添加一些打印(如注释中所示),可能导致工作线程有时放弃CPU
不,这是因为PrintStream::println
是同步方法。所有已知的JVM都将在CPU级别发出内存栅栏,以确保"获取"的语义。动作(在这种情况下,锁定获取),这将强制重新加载stop
变量。这不是规范所要求的,只是一种实现选择。
- 如果我将标志
boolean stop
标记为易失性,导致写入立即被工作线程看到
规范实际上对于挥发性写入何时必须对其他线程可见而没有挂钟时间要求,但实际上它被理解为它必须很快变得可见"。因此,这种更改是确保对stop
的写入安全地发布到其他读取它的线程并随后观察的正确方法。
- 如果我将计数器
t
标记为不稳定......为此我不知道是什么原因造成了不挂。
这些又是JVM确保volatile
读取语义的间接影响,这是另一种"获取"线程间行动。
总之,除了使变量stop
成为易变变量之外,由于底层JVM实现的意外副作用,您的程序将从永久挂起切换到完成,为简单起见,它会更多地刷新/失效线程本地状态比规范要求的。
答案 1 :(得分:2)
这可能是可能的原因:
如果您有兴趣深入研究这个主题,那么我建议您继续阅读Brian Goetz撰写的“Java Concurrency in Practice”#34;书。
答案 2 :(得分:0)
将变量标记为volatile是JVM的提示,在更新该变量时刷新/同步线程/核心之间的相关缓存段。将stop
标记为volatile然后具有更好的行为(但不完美,您可能会在线程上看到更新之前执行一些额外的执行)。
将t
标记为不稳定让我觉得它的工作原理,可能是因为这是一个如此小的程序,t
和stop
位于缓存的同一行,所以当一个人被冲洗/同步时,另一个人也会这样做。
System.out.println
是线程安全的,因此内部会进行一些同步。同样,这可能导致缓存的某些部分在线程之间同步。
如果有人可以添加,请做,我也很想听到更详细的答案。
答案 3 :(得分:0)
实际上,它所说的内容 - 提供了对多个线程之间的字段的一致访问,您可以看到它。
如果没有volatile
关键字,对字段的多线程访问不保证一致,编译器可以引入一些优化,例如在CPU寄存器中缓存它,或者不从CPU核心本地缓存写入外部存储器或共享缓存。
stop
和易变t
根据JSR-133 Java内存模型规范,在易失性字段更新之前所有写入(到任何其他字段)都可见,它们发生在之前。
在递增stop
后设置t
标志时,循环中的后续读取将无法看到它,但下一个递增( volatile-write )将使其成为可见。
Java Language Specification: 8.3.1.4. volatile Fields
An article about Java Memory Model, from the author of Java theory and practice