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)