在多线程的环境下,需要考虑代码的原子性、可见性、有序性。
原子性
public class Demo {
public static int count;
static void add() {
count += 2;
}
static void subtract() {
count -= 2;
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
add();
});
Thread thread2 = new Thread(() -> {
subtract();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count: " + count);
}
}
在上面代码的运行结果中,大部分的情况下 count 的值为 0,但是少部分会出现 2 或 -2,这就是由于 count += 2 和 count -= 2 这两段代码不是原子的。
count += 2 对应的反编译结果如下:
0: getstatic
3: iconst_2
4: iadd
5: putstatic
count -= 2 对应的反编译结果如下:
0: getstatic
3: iconst_2
4: isub
5: putstatic
这段指令的执行顺序可能如下所示:

我们可以看到,subtract 的运行在 add 代码中间,这就导致 subtract 运行结束后 count 的值为 -2,但是 add 读取一开始读取的 count 的值为 0,并在后续运行时一直按照 0 进行运行,就造成 add 运行结束后 count 的值为 2,将 subtract 的结果覆盖了。
若我们对 count 添加了 volatile 关键字后,也不能解决上面的问题,因为 volatile 并不能保证代码的原子性,这里可以使用加锁保证原子性,代码如下:
public class Demo {
public static int count;
public static final Object object = new Object();
static void add() {
count += 2;
}
static void subtract() {
count -= 2;
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized (object) {
add();
}
});
Thread thread2 = new Thread(() -> {
synchronized (object) {
subtract();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count: " + count);
}
}
可见性
public class Demo {
public static boolean flag;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
flag = true;
});
thread.start();
method();
}
public static void method() {
int count = 0;
while (!flag) {
count++;
}
System.out.println("while stop");
}
}
在上面的代码中,while 循环先运行,thread 线程休眠 1 秒后运行,当 flag 被改为 true 时,while 循环就会停止运行,代码运行结果如下:

可以看到,虽然此时 flag 已经变为 true,但是 while 循环依旧没有停止。
这是因为,while 循环一秒就可以运行接近百万次,但是这百万次需要从内存中读取 flag 的值,虽然一两次很快,但是累积起来就会有一定的时间。这时 JIT 就会对代码进行优化,JIT 发现在这百万次运行中,flag 的值都是 false,那么就会将 flag 直接替换为 false,导致即使 thread 线程将 flag 替换为 true,while 循环也无法感知到。
这时当我们将 thread 的休眠时间缩短为 1ms,代码的运行结果如下:

这里可以看见,while 循环停止了。这是因为在这 1ms 中,while 循环的执行次数少,JIT 不会对这段代码进行优化,当 flag 被 thread 改为 true 时,循环就会停止。
但是,缩短时间,总归不是一个好的解决办法,在这里有两个比较好的解决办法:
禁止 JIT 优化:在执行代码前添加 -Xint 参数,以禁止 JIT 优化,操作如下:

但是禁止 JIT 优化后,就会降低一定的代码运行效率使用 volatile 关键字:将 flag 使用 volatile 修饰,这样就能保证变量的可见性
有序性
public class Demo {
public static int x;
public static int y;
public static int a;
public static int b;
public static void method1() {
x = 1; //1
y = 2; //2
}
public static void method2() {
a = y; //3
b = x; //4
}
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
method1();
});
Thread thread2 = new Thread(() -> {
method2();
});
thread1.start();
thread2.start();
System.out.println("a: " + a);
System.out.println("b: " + b);
}
}
在上面的代码中,a 和 b 的值可能出现下面几种情况:
a = 0, b = 0,代码运行顺序为:3 -> 4 -> 1 -> 2a = 0, b = 1,代码运行顺序为:1 -> 3 -> 4 -> 2a = 2, b = 1,代码运行顺序为:1 -> 2 -> 3 -> 4
但是会不会出现 a = 2, b = 0 的情况呢?
答案是可能的。这是由于由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致指令的实际执行顺序与编写顺序不一致,让 2 跑到了 1 之前运行,运行顺序为:2 -> 3 -> 4 -> 1。
对于这种情况,可以使用 volatile 关键字修饰 y 变量,这样就能预防上面的情况。
但是 volatile 修饰的变量也是有讲究的,若让 volatile 修饰 x,就不会起到作用。
所以,更好的方式是加锁,让 thread1 先运行,再让 thread2 运行。


