考虑以下代码:
public class Main {
public static void main(String args[]){
Thread t1 = new SimpleThread();
Thread t2 = new SimpleThread();
t1.start();
t2.start();
}
static class SimpleThread extends Thread {
private static volatile int n = 0;
public void run() {
n++;
int m = n;
System.out.println(m);
}
}
}
该程序所有可能的输出是什么?为什么?
我想到了以下场景:
1 2
n++
都“正确”执行,但调度程序首先执行具有 m=2
的线程的打印,所以我们有 2 1
n=0
中读取 n++
,所以我们有 1 1
我对这些场景的理解正确吗?是否有一些我错过的场景(例如,
2 2
是否会发生以及为什么)
?
++
增量运算符被视为非原子你的代码
n++
是不是原子的。或者说,我们必须假设 Java 中 Postfix 增量运算符 ++
的定义。请参阅 Java 语言规范,其中没有做出线程安全 或原子性 的承诺。另请参阅 为什么 i++ 不是原子的?。
所以时间上有三个时刻:
n
读取当前值。 (本质上是复制该值)1
添加到该值。这些时刻之间可能会经过任意时间。在此期间,另一个线程可以访问和/或更改变量内的值。因此第一个线程可以践踏第二个线程设置的值。
volatile
保证可见性,但不保证线程安全private static volatile int n = 0;
那里的
static
表示应用程序中仅存在一个名为 n
的变量。
&n++;
int m = n;
这两条线之间可能会经过任意时间。在此期间,另一个线程可能会更改我们唯一的
n
变量的值。
顶部行中的
volatile
意味着当前线程将看到任何更改,保证。
因此,在这一对行的第一行中设置的值可能与第二行中访问的值很不同。
至于您的场景:
1
& 2
作为输出。如果我们结合上面讨论的事实:
m = n
线是非原子的…然后我们有四个与
n
相关的步骤的序列,这些步骤可能会交错:
n
的值,制作副本。n
。n
中的值(复制,存储在m
中)。交错场景包括:
1
和 1
0
,并将 1
写入变量。第二个作家践踏了第一个作家。控制台输出为 1
& 1
。1
& 2
or 2
& 1
0
,写入 1
,然后读取 1
。另一个线程读取 1
并写入 2
,然后读取 2
。没有踩踏,因为读写是连续的。但请注意,在这种情况下,连续的 println
调用可能不是连续的。 println
调用可能会交错。因此控制台输出可能按以下任一顺序: 1
& 2
或 2
& 1
。2
& 2
0
,并写入 1
...,但其他线程读取 1
并写入 2
...,第一个线程读取 2
。第一个线程写入了 1
,但在为 n
赋值检索 m = n
的当前值之前,n
中的值已被另一个线程更改。您的问题中未涵盖
2
和 2
的最后一个案例。感谢 Mark Rotteveel 在评论中指出了这个案例。花了点时间才看到!
警告:我不是这方面的专家。如需专业知识,请参阅《Java 并发实践》一书。