1 Java内存模型

Java内存模型的主要目的是定义程序中各种变量的访问规则,即把变量值存储到内存和从内存中取出变量值这样的底层细节。此处的变量包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数。

  • 所有变量都存储在主内存(Main Memory)中,每条线程有自己的工作内存(Working Memeory)

  • 线程对变量的读取、赋值,必须在工作内存中进行。

  • 内存间交互操作都是原子的,包括:

    • lock(锁定):作用于主内存的变量,把变量标识为一条线程独占的状态。
    • unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放之后才可以被其他线程锁定。
    • read(读取):作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中。
    • load(载入):作用于工作内存的变量,把read操作从主内存得到的变量值放入工作内存的变量副本中。
    • use(使用):作用于工作内存的变量,把工作内存中的一个变量的值传递给执行引擎。
    • assign(赋值):作用于工作内存的变量,把一个从执行引擎接收的值赋给工作内存中的变量。
    • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中。
    • write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。
  • 在执行上述8种基本操作时必须满足如下规则:

    • 不允许read和load、store和write操作之一单独出现。
    • 不允许一个线程丢弃它最近的assign操作。
    • 不允许一个线程无原因地(没有发生任何assign操作)把数据从线程的工作内存同步回主内存。
    • 一个新的变量只能在主内存中产生,不允许工作内存中直接使用一个未被初始化(load或assign)的变量。即对一个变量实施use、store操作之前,必须先执行assign和load操作。
    • 对一个变量执行lock操作,将清空工作内存中次变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作以初始化变量的值。
    • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
    • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

2 volatile

当一个变量被volatile修饰后,它将具备两项特性:

  • 保证此变量对所有线程的可见性
  • 禁止指令重排序优化

以下是Java内存模型对volatile的特殊规则:

假设T表示一个线程,V和W分别表示两个volatile变量,那么在进行read、load、use、assign、store、write操作时需要满足以下规则:

  • 只有当线程T对变量V执行的前一个动作是load时,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的最后一个动作是use的时候,线程T才能对变量执行load动作。

    这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改。

  • 只有当线程T对变量执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store时,线程T才能对变量执行assign动作。

    这条规则要求在工作内存中,每次修改V后都必须立刻同步回到主内存中,用于保证其他线程可以看到自己对变量V所做的修改。

  • 假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;与此类似,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read动作。如果A先于B,那么P先于Q。

    这条规则要求volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序于程序的顺序相同。

2.1 可见性

可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是立即可知的。

下面是一种错误地使用volatile的例子:

public class VolatileErrorDeme {

    public static volatile int race = 0;
    public static void increase() {
        race++;
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    increase();
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(race);
    }
}

由于volatile只能保证可见性,因此仍然需要使用synchronized/juc中的锁/原子类来保证原子性

//    public static volatile int race = 0;
//    public static void increase() {
//        synchronized(VolatileErrorDeme.class){
//            race++;
//        }
//    }

//    static AtomicInteger next = new AtomicInteger();
//    public static void increase() {
//        race = next.incrementAndGet();
//    }

以下是volatile的使用场景:

volatile boolean b;
public void shutdown() {
        b = true;
}
public void doWork(){
        while(!b){
            //doWork
    }
}

2.2 禁止指令重排序

volatile boolean b = false;

// 假设以下代码在线程A中执行
loadConfig();
b = true;

// 假设以下代码在线程B中执行
while(!b){
    sleep();
}

doWork();

如果上述代码中没有volatile修饰,那么b = true可能由于指令重排序而先于loadConfig()执行,此时将出现还没加载完配置信息,便执行了doWork()

volatile是如何禁止指令重排序的?

有volatile修饰的变量,赋值后多执行了一个lock操作,其作用相当于内存屏障,指重排序时不能把后面的指令重排序到内存屏障之前的位置。

2.3 使用volatile的意义

  • volatile读的性能与普通变量相差不大,但是写的性能会慢一些
  • volatile的同步机制要优于锁(synchronized和juc.lock)