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

Yos*_*ein 8 java multithreading volatile

因此,在接受一些求职面试之后,我想编写一个小程序来检查i++ java中的非原子性,并且在实践中应该添加一些锁定来保护它.原来你应该,但这不是问题.

所以我在这里写这个程序只是为了检查它.

事情是,它挂起了.似乎主线程被挂t1.join() 在线上,即使两个工作线程都应该因为stop = true前一行而完成.

我发现悬挂停止如果:

  • 我在工作线程中添加一些打印(如注释中所示),可能导致工作线程有时放弃CPU或
  • 如果我将标志标记boolean stopvolatile,导致写入立即被工作线程看到,或者
  • 如果我将计数器标记tvolatile...为此,我不知道是什么导致了不挂.

有人可以解释发生了什么吗?为什么我会看到悬挂,为什么它会在这三种情况下停止?

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Mar*_*nik 8

似乎主线程被挂t1.join()在线上,即使两个工作线程都应该因为stop = true前一行而完成.

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

if (!stop) {
    while (true) {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

这种特殊的优化被称为循环读取动作的"提升".

我发现悬挂停止如果:

  • 我在工作线程中添加一些打印(如注释中所示),可能导致工作线程有时放弃CPU

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

  • 如果我将标志标记boolean stop为volatile,则工作线程会立即看到写入

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

  • 如果我将计数器标记t为易变...为此我不知道是什么导致了不挂.

这些又是JVM确保volatile读取语义的间接影响,这是另一种"获取"线程间动作.

总之,除了更改stop生成volatile变量之外,由于底层JVM实现的意外副作用,您的程序从永久挂起切换到完成,为简单起见,线程本地状态的某些更多冲洗/失效比规范.