Java volatile 变量多线程行为
Java volatile variable multithreading behavior
最近面试面试,对这道题摸不着头脑。我会把我解释过的和我想知道什么是正确的行为放在一起。我想确保我的理解是正确的,不是为了面试,而是为了变得更好。
**问题:**我们下面有一个计数器(java 代码),如果说 20 个线程 运行 这个代码是并行的,那么 a 和 b 的值会相同吗?如果 b 不可变怎么办
我的回答:可能是也可能不是。为什么 - 当存在竞争条件时,没有 gua运行teee 不同线程将看到 b 或 a 的更新值,在这种情况下,值会不同。在这里,我认为 volatile 没有任何区别。
我写了一个简单的客户端,运行这段代码在 16 核笔记本电脑上的线程数最多为 50,当我尝试 运行 500 次时,我可以看到 a 和 b 的相同值。但是,当我将线程数增加到 200 时,有时我会看到不同的值。
问题 - 正确答案是什么?我的理解是否正确?为什么我在笔记本电脑上看到了不同的结果?
public class VolatileCounter implements Runnable {
private static int a;
private volatile static int b;
public VolatileCounter() {
}
@Override
public void run() {
try{
b++;
a++;
}finally {
//some system println....
}
}
public void printNumbers() {
System.out.println(b);
System.out.println(a);
}
}
即使有 50 个线程,该值也可能不同,这完全取决于竞争条件和上下文切换。由于a和b是原始类型,所以它是否是volatile并不重要
大部分问题是递增运算符不是线程安全的,因为它采取了多个步骤:首先它们必须获取当前值,然后递增,然后用新值更新字段,给其他线程机会交错操作并混淆结果。参见 Is the pre-increment operator thread-safe?。 (如果我向他们展示类似的东西,这将是我希望从我正在采访的人那里听到的要点。)
一些观察:
仅仅因为并发问题可以发生并不意味着它一定会发生。提高并发级别确实会增加出现问题的机会,这就是您所观察到的。
这并不是说内存可见性不起作用,只是很难将其与本示例中的非线程安全运算符行为区分开来。如果这里没有 volatile 关键字,更新是否可见取决于 JVM 实现。 PC 上的 JVM 通常相当宽容。其他平台可能不是。
如果您在代码中包含 printlns,它们会在控制台上获得锁定,因此它们会影响多线程行为和更新的可见性。
有一个称为搭载的内存可见性技巧也可能发挥作用并影响 class 变量 a
的更新是否可见,请参阅:Volatile piggyback. Is this enough for visiblity?
如果您像这样启动 50 个线程,那么启动这些线程会花费很长时间,因此很有可能它们不会 运行 同时启动。如果它们确实同时发生在 运行 上,您可能会发现这两种情况下的数字都略低于预期。
注意:使用 volatile
字段不仅会影响字段 b
,还会影响 b
之前和之后的所有内存访问,即它还会影响 a
.
自增操作实际上是加载、自增和存储,所以实际上不是线程安全的。
最近面试面试,对这道题摸不着头脑。我会把我解释过的和我想知道什么是正确的行为放在一起。我想确保我的理解是正确的,不是为了面试,而是为了变得更好。
**问题:**我们下面有一个计数器(java 代码),如果说 20 个线程 运行 这个代码是并行的,那么 a 和 b 的值会相同吗?如果 b 不可变怎么办
我的回答:可能是也可能不是。为什么 - 当存在竞争条件时,没有 gua运行teee 不同线程将看到 b 或 a 的更新值,在这种情况下,值会不同。在这里,我认为 volatile 没有任何区别。
我写了一个简单的客户端,运行这段代码在 16 核笔记本电脑上的线程数最多为 50,当我尝试 运行 500 次时,我可以看到 a 和 b 的相同值。但是,当我将线程数增加到 200 时,有时我会看到不同的值。
问题 - 正确答案是什么?我的理解是否正确?为什么我在笔记本电脑上看到了不同的结果?
public class VolatileCounter implements Runnable {
private static int a;
private volatile static int b;
public VolatileCounter() {
}
@Override
public void run() {
try{
b++;
a++;
}finally {
//some system println....
}
}
public void printNumbers() {
System.out.println(b);
System.out.println(a);
}
}
即使有 50 个线程,该值也可能不同,这完全取决于竞争条件和上下文切换。由于a和b是原始类型,所以它是否是volatile并不重要
大部分问题是递增运算符不是线程安全的,因为它采取了多个步骤:首先它们必须获取当前值,然后递增,然后用新值更新字段,给其他线程机会交错操作并混淆结果。参见 Is the pre-increment operator thread-safe?。 (如果我向他们展示类似的东西,这将是我希望从我正在采访的人那里听到的要点。)
一些观察:
仅仅因为并发问题可以发生并不意味着它一定会发生。提高并发级别确实会增加出现问题的机会,这就是您所观察到的。
这并不是说内存可见性不起作用,只是很难将其与本示例中的非线程安全运算符行为区分开来。如果这里没有 volatile 关键字,更新是否可见取决于 JVM 实现。 PC 上的 JVM 通常相当宽容。其他平台可能不是。
如果您在代码中包含 printlns,它们会在控制台上获得锁定,因此它们会影响多线程行为和更新的可见性。
有一个称为搭载的内存可见性技巧也可能发挥作用并影响 class 变量
a
的更新是否可见,请参阅:Volatile piggyback. Is this enough for visiblity?
如果您像这样启动 50 个线程,那么启动这些线程会花费很长时间,因此很有可能它们不会 运行 同时启动。如果它们确实同时发生在 运行 上,您可能会发现这两种情况下的数字都略低于预期。
注意:使用 volatile
字段不仅会影响字段 b
,还会影响 b
之前和之后的所有内存访问,即它还会影响 a
.
自增操作实际上是加载、自增和存储,所以实际上不是线程安全的。