Java 并发-内存模型

151 阅读2分钟

Java 内存模型

可见性、原子性、有序性问题是并发编程中的 BUG 源头,是编程领域的共性问题,可见性和原子性的产生原因是缓存和指令重排,Java 语言提供了内存模型来规避此类问题

Java 内存模型是一个复杂的规范,基本的原理就是禁用缓存和指令重排,提供给开发者的手段就是volatilesynchronizedfinal 三个关键字,以及六项Happens-Before规则

volatile 关键字

  1. volatile 关键字在许多语言中都有用到,原始含义就是禁用 CPU 缓存

  2. volatile 解决可见性问题

    public class App {
        volatile static boolean flag = true;
        public static void main(String[] args) throws InterruptedException {
            new Thread(() -> {
                while (flag) {
                    // 
                }
                System.out.println("end");
            }).start();
            TimeUnit.SECONDS.sleep(5);
            flag = false;
        }
    }
    

    主线程修改 flag,子线程内的死循环是否会结束?

    如果不使用 volatile 声明,子线程一直会读取缓存内的值,而看不见其他线程的修改

    使用 volatile 关键字,强制线程通过内存对该变量进行读写,而不是 CPU 缓存

  3. volatile 解决有序性问题

    在 JDK1.5 之前,volatile 被设计用来禁用缓存,但是并不关心指令的排序问题

    在 JDK1.5 对 volatile 进行了增强,具体内容就是增加了一项 Happens-Before 规则

Happens-Before 规则

Happens-Before 规则是 Java 内存模型规范的重要部分

规则要表达的核心内容是在特定场景下,前面一个操作的结果对后续操作是可见的

  1. 程序的顺序性规则

    在单线程中对一个变量的操作对后续的操作是可见的

  2. volatile 变量规则

    对一个 volatile 变量的写操作对后续的读操作是可见的(多线程的顺序性规则)

  3. 传递性规则

    即 A 操作对 B 操作可见,B 操作对 操作可见,那么 A 操作对 C 操作可见

  4. 管程中锁的规则

    一个线程的解锁操作对下一个线程加锁操作是可见的,如使用 synchronized

  5. 线程 start 规则

    主线程调用子线程的 start 方法对子线程中的操作是可见的

  6. 线程 join 规则

    子线程中的操作对主线程 join 方法是可见的

final 关键字

  1. 重排序可能导致一个线程看到一个对象的时候,这个对象还没有初始化完毕

    部分初始化或者完全没有经过初始化,因为普通变量的写入是可以被重排序到构造器之外的

  2. Java 内存模型要求编译器对 final 关键字特殊处理

    即禁止处理器将 final 变量初始化操作重排序到构造器之外