一个线程修改了变量,另一个线程什么时候能读到这个值?比如主线程启动一个任务,再用 join() 等它结束,为什么就能拿到任务写入的结果?这些问题涉及 Java 内存模型,也就是 JMM。
三个容易混淆的概念
原子性关注一次操作会不会被其他线程的操作穿插。count++ 包含读取、加一、写回,单独把 count 声明为 volatile 也不能把三步合成一个原子操作。
可见性关注一个线程的写入,什么时候能被另一个线程看见。共享变量没有正确同步时,不能要求读线程一定看到最新值。
有序性关注操作之间允许怎样重排。编译器和处理器可以做优化,只要没有破坏 Java 内存模型允许观察到的结果。
所谓“主内存”和“工作内存”,可以帮助理解线程之间为什么需要同步,但它们是模型中的概念,不应直接等同于某一级 CPU 缓存。也不要把 JMM 与堆、栈、方法区这样的运行时内存划分混为一谈。
happens-before
happens-before 用来表达操作之间的可见性和顺序保证。先看 start() 和 join() 的例子:
public class HappensBeforeDemo {
private static int input;
private static int result;
public static void main(String[] args) throws InterruptedException {
input = 21;
Thread worker = new Thread(() -> result = input * 2);
worker.start();
worker.join();
System.out.println(result);
}
}结果为 42。启动前对 input 的写入,对新线程可见;join() 成功等到线程结束后,主线程也能看见它对 result 的写入。这里两个字段都不需要额外声明为 volatile。
类似的关系还有:同一线程中的程序顺序、对同一个监视器先解锁再加锁、对同一个 volatile 变量先写再读。happens-before 关系还可以传递,下面的例子会用到这一点。
如果删除 join(),主线程可能在任务执行前就打印 result。改成 sleep(1000) 也不等于建立了同样的同步关系,休眠只是在时间上推迟执行。
从标记发布数据
public class PublishDemo {
private static int value;
private static volatile boolean ready;
public static void main(String[] args) throws InterruptedException {
Thread reader = new Thread(() -> {
while (!ready) {
Thread.yield();
}
System.out.println(value);
});
reader.start();
value = 42;
ready = true;
reader.join();
}
}读线程观察到 ready 为 true 后,能够看到它之前写入的 value。起作用的是 volatile 的读写关系以及传递性,不是 yield();yield() 只是给调度器一个提示,不能用来修复可见性问题。
这里 value 只写入一次,再通过 ready 通知读线程。如果要反复更新,写线程还需要知道上一轮的数据是否已经被读取,单靠把 ready 改回 false 无法完成这一步协调。