Java 多线程 - 加入一个 CPU 重线程和 volatile 关键字
Java multithreading - joining a CPU heavy thread and volatile keyword
所以,在一些工作面试之后,我想写一个小程序来检查 i++
在 java 中是否真的是非原子的,并且在实践中应该添加一些锁定到保护它。原来你应该,但这不是这里的问题。
所以我在这里写了这个程序只是为了检查一下。
问题是,它挂了。似乎主线程卡在 t1.join()
行,即使由于上一行的 stop = true
,两个工作线程都应该完成。
我发现在以下情况下挂起会停止:
- 我在工作线程中添加了一些打印(如评论中所述),可能导致工作线程有时会放弃 CPU
或
- 如果我将标志
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;
}
}
这些可能是可能的原因:
- 将 "stop" 标记为易失性可防止其值缓存在工作线程状态(例如寄存器)中
- 将 "t" 标记为易失性可确保在访问 "t" 时读取更新的值。但是,如果 JVM 将这些变量彼此靠近排列,此行为也可能会获得 "stop" 的更新值,因此它们可能会被读取并存储在同一个 "cache line" 中。尝试添加 @Contended 注释以查看在这种情况下行为是否仍然存在。您可以在以下位置找到更多信息:https://en.wikipedia.org/wiki/False_sharing , https://mechanical-sympathy.blogspot.am/2011/07/false-sharing.html , https://blogs.oracle.com/dave/entry/java_contented_annotation_to_help
- 调用 "System.out.println()" 实际上执行系统调用,因此转换到 "native calls stack",这也可能使 "processor cache" 被清除。 What does a JVM have to do when calling a native method?
如果您有兴趣深入研究该主题,那么我建议您阅读 "Java Concurrency in Practice by Brian Goetz" 这本书。
将变量标记为 volatile 是 JVM 的一个提示,当该变量更新时 flush/sync threads/cores 之间的相关缓存段。将 stop
标记为 volatile 然后会有更好的行为(但并不完美,您可能会在线程看到更新之前执行一些额外的操作)。
将 t
标记为 volatile 让我很困惑为什么它会起作用,可能是因为这是一个很小的程序,t
和 stop
在同一行中缓存,所以当一个获得 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 的线程局部状态比规范要求的要多。
所以,在一些工作面试之后,我想写一个小程序来检查 i++
在 java 中是否真的是非原子的,并且在实践中应该添加一些锁定到保护它。原来你应该,但这不是这里的问题。
所以我在这里写了这个程序只是为了检查一下。
问题是,它挂了。似乎主线程卡在 t1.join()
行,即使由于上一行的 stop = true
,两个工作线程都应该完成。
我发现在以下情况下挂起会停止:
- 我在工作线程中添加了一些打印(如评论中所述),可能导致工作线程有时会放弃 CPU 或
- 如果我将标志
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;
}
}
这些可能是可能的原因:
- 将 "stop" 标记为易失性可防止其值缓存在工作线程状态(例如寄存器)中
- 将 "t" 标记为易失性可确保在访问 "t" 时读取更新的值。但是,如果 JVM 将这些变量彼此靠近排列,此行为也可能会获得 "stop" 的更新值,因此它们可能会被读取并存储在同一个 "cache line" 中。尝试添加 @Contended 注释以查看在这种情况下行为是否仍然存在。您可以在以下位置找到更多信息:https://en.wikipedia.org/wiki/False_sharing , https://mechanical-sympathy.blogspot.am/2011/07/false-sharing.html , https://blogs.oracle.com/dave/entry/java_contented_annotation_to_help
- 调用 "System.out.println()" 实际上执行系统调用,因此转换到 "native calls stack",这也可能使 "processor cache" 被清除。 What does a JVM have to do when calling a native method?
如果您有兴趣深入研究该主题,那么我建议您阅读 "Java Concurrency in Practice by Brian Goetz" 这本书。
将变量标记为 volatile 是 JVM 的一个提示,当该变量更新时 flush/sync threads/cores 之间的相关缓存段。将 stop
标记为 volatile 然后会有更好的行为(但并不完美,您可能会在线程看到更新之前执行一些额外的操作)。
将 t
标记为 volatile 让我很困惑为什么它会起作用,可能是因为这是一个很小的程序,t
和 stop
在同一行中缓存,所以当一个获得 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 thestop = 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 的线程局部状态比规范要求的要多。