03 volatile

volatile 关键字使一个变量在多个线程间可见

A、B 线程都有用到一个变量,java 默认是 A 线程缓冲区保留一份 copy,这样如果 B 线程修改了变量,则 A 线程未必知道。使用 volatile 关键字,当 B 线程修改了变量的值,会告知 A 线程缓冲区的变量已过期,A 线程就会刷新缓冲区,从主内存中读到变量的修改值(修改 -> 通知 -> 刷新)。

public class T {
// 不加 volatile,程序永远不会停下
/* volatile */ boolean running = true;
public void m() {
System.out.println("m start");
while (running) {
System.out.println("running");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("m end");
}
public static void main(String[] args) {
T t = new T();
/*
需要注意,如果直接执行 t.m() 而不是另起线程,那么无论是否加 volatile,程序都不会停下,循环判断中的 running 一直为 true
*/
new Thread(t::m).start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
System.out.println(t.running);
}
}

volatile 并不能保证多个线程共同修改 running 变量时所带来的不一致问题,因为 volatile 只保证了可见性,并不保证原子性,所以 volatile 并不能替代 synchronized

以下栗子可以看出区别,当不加 synchronized 时,最终结果远小于 100000,其错误原因:假设当前值 count 为 100,两个线程 A、B 同时读到的值都是 100(保证了可见性),这时 A、B 线程都执行了 +1 操作,先后覆盖写入 count 值 101,他们不会去检查 count 是否已经是 101,所以两次 +1 操作只实现了一次效果。

public class T {
volatile private int count = 0;
// 不加 synchronized,最终结果远小于 10 * 10000
public /* synchronized */ void m() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
public static void main(String[] args) {
T t = new T();
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threadList.add(new Thread(t::m, "Thread" + i));
}
threadList.forEach(Thread::start);
threadList.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("count: " + t.count);
}
}

其实,除了上面 synchronized 解决方法外,java 提供的 AtomicInteger 相关类也可以解决。因为 count.getAndIncrement() 是一个原子操作,而 count++ 不是。

public class T {
private AtomicInteger count = new AtomicInteger(0);
public void m() {
for (int i = 0; i < 10000; i++) {
count.getAndIncrement();
}
}
public static void main(String[] args) {
T t = new T();
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threadList.add(new Thread(t::m, "Thread" + i));
}
threadList.forEach(Thread::start);
threadList.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("count: " + t.count.get());
}
}

不过需要注意的是 AtomicInteger 本身方法是原子性的,但不能保证多个方法的连续调用是原子性的。再举个栗子,我们将上面栗子稍微修改一下,加一个判断,我们预期最后 count 值为 1000,但是实际结果却会大于 1000,因为虽然 count.get()count.getAndIncrement() 都是原子操作,但是两者之间却不是,所以会出现当前 count 值为 999,A 线程 count.get() < 1000 判断为真进入循环后,此时 B 线程执行加一操作后值为 1000,A 线程依旧执行加一操作,导致最终结果偏大。

public void m() {
for (int i = 0; i < 10000; i++) {
if (count.get() < 1000) {
count.getAndIncrement();
}
}
}