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

Java multithreading - joining a CPU heavy thread and volatile keyword

所以,在一些工作面试之后,我想写一个小程序来检查 i++ 在 java 中是否真的是非原子的,并且在实践中应该添加一些锁定到保护它。原来你应该,但这不是这里的问题。

所以我在这里写了这个程序只是为了检查一下。

问题是,它挂了。似乎主线程卡在 t1.join() 行,即使由于上一行的 stop = true,两个工作线程都应该完成。

我发现在以下情况下挂起会停止:

谁能解释一下这是怎么回事?为什么我会看到挂起,为什么在这三种情况下它会停止?

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

这些可能是可能的原因:

如果您有兴趣深入研究该主题,那么我建议您阅读 "Java Concurrency in Practice by Brian Goetz" 这本书。

将变量标记为 volatile 是 JVM 的一个提示,当该变量更新时 flush/sync threads/cores 之间的相关缓存段。将 stop 标记为 volatile 然后会有更好的行为(但并不完美,您可能会在线程看到更新之前执行一些额外的操作)。

t 标记为 volatile 让我很困惑为什么它会起作用,可能是因为这是一个很小的程序,tstop 在同一行中缓存,所以当一个获得 flushed/synced 时,另一个也获得。

System.out.println 是线程安全的,因此内部会进行一些同步。同样,这可能会导致缓存的某些部分在线程之间同步。

如果有人可以补充,请添加,我也很想听到更详细的答案。

它确实做到了它所说的 -- 在多个线程之间提供对字段的一致访问,您可以看到它。

没有volatile关键字,不保证多线程访问该字段是一致的,编译器可以引入一些优化,比如将它缓存在CPU寄存器中,或者不从[=中写出40=]核心本地缓存到外部内存或共享缓存。


对于具有非易失性 stop 和易失性 t

的部分

根据 JSR-133 Java 内存模型规范,可变字段更新之前的所有写入(对任何其他字段)都是可见的,它们是 happened-before动作。

当你在递增t后设置stop标志时,它在循环中的后续读取将不可见,但下一次递增(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

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.

在没有 volatile、锁定或其他安全发布机制的情况下,JVM 没有义务永远 使 stop = true 对其他线程可见.特别适用于您的情况,当您的主线程休眠一秒钟时,JIT 编译器将您的 while (!stop) 热循环优化为相当于

if (!stop) {
    while (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

不是,因为PrintStream::println是同步方法。所有已知的 JVM 都会在 CPU 级别发出内存栅栏,以确保“获取”操作(在本例中为锁定获取)的语义,这将强制重新加载 stop 变量。这不是规范要求的,只是一个实现选择。

  • If I mark the flag boolean stop as volatile, causing the write to immediately be seen by worker threads

该规范实际上没有关于易失性写入何时必须对其他线程可见的挂钟时间要求,但实际上可以理解它必须“很快”变得可见。因此,此更改是确保对 stop 的写入安全发布到其他读取它的线程并随后被其他线程观察到的正确方法。

  • If I mark the counter t as volatile... for this I have no idea what causes the un-hanging.

这些又是 JVM 确保 volatile 读取语义的间接影响,这是另一种“获取”线程间操作。

总而言之,除了使 stop 成为可变变量的更改外,由于底层 JVM 实现的意外副作用,您的程序从永远挂起切换到完成,为了简单起见,它做了更多 flushing/invalidation 的线程局部状态比规范要求的要多。