简体   繁体   English

Java多线程 - 加入CPU重线程和volatile关键字

[英]Java multithreading - joining a CPU heavy thread and volatile keyword

So, follwoing some job interviews I wanted to write a small program to check that i++ really is non-atomic in java, and that one should, in practice,add some locking to protect it. 因此,在接受一些求职面试之后,我想编写一个小程序来检查i++在java中是否真的是非原子的,并且实际上应该添加一些锁定来保护它。 Turns out you should, but this is not the question here. 原来你应该,但这不是问题。

So I wrote this program here just to check it. 所以我在这里写这个程序只是为了检查它。

The thing is, it hangs. 事情是,它挂起了。 It seems that the main thread is stuck on on t1.join() line, even though both worker threads should finish because of the stop = true from previous line. 似乎主线程停留在t1.join()行上,即使两个工作线程都应该因为前一行的stop = true完成。

I found that the hanging stops if : 我发现悬挂停止如果:

  • I add some printing inside the worker threads (as in the comments), probably causing the worker threads to sometime give up CPU or 我在工作线程中添加了一些打印(如注释中所示),可能导致工作线程有时放弃CPU或
  • If I mark the flag boolean stop as volatile , causing the write to immediately be seen by worker threads, or 如果我将标志boolean stop标记为volatile ,导致写入立即被工作线程看到,或者
  • If I mark the counter t as volatile ... for this I have no idea what causes the un-hanging. 如果我将计数器t标记为volatile ...为此我不知道是什么导致了不挂。

Can someone explain what's going on? 有人可以解释发生了什么吗? why do I see the hang and why does it stop in those three cases? 为什么我会看到悬挂,为什么它会在这三种情况下停止?

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;
    }
}

It seems that the main thread is stuck on on t1.join() line, even though both worker threads should finish because of the stop = true from previous line. 似乎主线程停留在t1.join()行上,即使两个工作线程都应该因为前一行的stop = true完成。

In the absence of volatile , locking, or other safe publication mechanism, the JVM has no obligation to ever make stop = true visible to other threads. 在没有volatile ,锁定,或其他安全发布机制,在JVM没有义务永远stop = true可见的其他线程。 Specifically applied to your case, while your main thread sleeps for one second, the JIT compiler optimizes your while (!stop) hot loop into the equivalent of 特别适用于您的情况,当您的主线程休眠一秒钟时,JIT编译器将您的while (!stop)热循环优化为相当于

if (!stop) {
    while (true) {
        ...
    }
}

This particular optimization is known as "hoisting" of the read action out of the loop. 这种特殊的优化被称为循环读取动作的“提升”。

I found that the hanging stops if : 我发现悬挂停止如果:

  • I add some printing inside the worker threads (as in the comments), probably causing the worker threads to sometime give up CPU 我在工作线程中添加一些打印(如注释中所示),可能导致工作线程有时放弃CPU

No, it's because PrintStream::println is a synchronized method. 不,这是因为PrintStream::println是一个同步方法。 All known JVMs will emit a memory fence at the CPU level to ensure the semantics of an "acquire" action (in this case, lock acquisition), and this will force a reload of the stop variable. 所有已知的JVM都将在CPU级别发出内存栅栏,以确保“获取”操作的语义(在这种情况下,锁定获取),这将强制重新加载stop变量。 This is not required by specification, just an implementation choice. 这不是规范所要求的,只是一种实现选择。

  • If I mark the flag boolean stop as volatile, causing the write to immediately be seen by worker threads 如果我将标志boolean stop标记为volatile,则工作线程会立即看到写入

The specification actually has no wall clock-time requirements on when a volatile write must become visible to other threads, but in practice it is understood that it must become visible "very soon". 该规范实际上没有关于何时易失性写入必须对其他线程可见的挂钟时间要求,但实际上,它必须“很快”变得可见。 So this change is the correct way to ensure that the write to stop is safely published to, and subsequently observed by, other threads reading it. 因此,这种更改是确保将写入stop安全地发布到其他读取它的线程并随后观察的正确方法。

  • If I mark the counter t as volatile... for this I have no idea what causes the un-hanging. 如果我将计数器t标记为易变...为此我不知道是什么导致了不挂。

These are again the indirect effects of what the JVM does to ensure the semantics of a volatile read, which is another kind of a "acquire" inter-thread action. 这些又是JVM确保volatile读取语义的间接影响,这是另一种“获取”线程间动作。

In summary, except for the change making stop a volatile variable, your program switches from hanging forever to completing due to the accidental side-effects of the underlying JVM implementation, which for simplicity does some more flushing/invalidation of thread-local state than required by the specification. 总之,除了更改stop一个volatile变量之外,由于底层JVM实现的意外副作用,您的程序从永久挂起切换到完成,为简单起见,线程本地状态的某些更多冲洗/失效比需要的更多按规格说明。

Those might be the possible reasons : 这可能是可能的原因:

If you are interested in digging deeper into the topic then I would suggest to go over "Java Concurrency in Practice by Brian Goetz" book. 如果您有兴趣深入研究该主题,那么我建议您阅读“Brian Goetz实践中的Java并发”一书。

Marking a variable as volatile is a hint for the JVM, to flush/sync the related segments of cache between threads/cores when that variable is updated. 将变量标记为volatile是JVM的提示,用于在更新该变量时刷新/同步线程/核心之间的相关缓存段。 Marking stop as volatile then has better behaviour (but not perfect, you may have some extra executions on your threads before they see the update). stop标记为易失性然后具有更好的行为(但不完美,您可能在看到更新之前在线程上执行一些额外的执行)。

Marking t as volatile puzzles me as to why it works, it may be that because this is such a small program, t and stop are in the same row in the cache, so when one gets flushed/synced the other does too. t标记为易失性使我觉得它的工作原理,可能是因为这是一个如此小的程序, tstop在缓存中的同一行,所以当一个人被刷新/同步时,另一个人也会这样做。

System.out.println is thread safe, so there is some synchronization going on internally. System.out.println是线程安全的,因此内部会进行一些同步。 Again, this may be causing some parts of the cache to be synced between the threads. 同样,这可能导致高速缓存的某些部分在线程之间同步。

If anyone can add to this, please do, I also would really like to hear a more detailed answer on this. 如果有人可以添加,请做,我也很想听到更详细的答案。

It does, actually, what it said to -- provides consistent access to the field between multiple threads, and you can see it. 事实上,它确实如此说 - 在多个线程之间提供对字段的一致访问,您可以看到它。

Without volatile keyword, multithread access to the field is not guaranteed to be consistent, compilers can introduce some optimisations, like caching it in the CPU register, or not writing out from CPU core local cache to external memory or shared cache. 如果没有volatile关键字,则不保证对字段的多线程访问是一致的,编译器可以引入一些优化,例如在CPU寄存器中缓存它,或者不从CPU核心本地缓存写入外部存储器或共享缓存。


For part with non-volatile stop and volatile t 对于具有非挥发性stop和挥发性t

According to JSR-133 Java Memory Model specification, all writes (to any other field) before volatile field update are made visible, they are happened-before actions. 根据JSR-133 Java内存模型规范,在易失性字段更新之前的所有写入(到任何其他字段)都是可见的,它们是操作之前发生的

When you set stop flag after incrementing t , it will be not visible by subsequent read in the loop, but next increment ( volatile-write ) will make it visible. 在递增t后设置stop标志时,循环中的后续读取将无法看到它,但下一次递增( volatile-write )将使其可见。


See also 也可以看看

Java Language Specification: 8.3.1.4. Java语言规范:8.3.1.4。 volatile Fields 易变的领域

An article about Java Memory Model, from the author of Java theory and practice 一篇关于Java Memory Model的文章,来自Java理论与实践的作者

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM