一个线程修改了变量,另一个线程什么时候能读到这个值?比如主线程启动一个任务,再用 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 无法完成这一步协调。

最后修改:2018 年 01 月 31 日
如果觉得我的文章对你有用,请随意赞赏