在单线程程序中,操作系统会通过编译器优化重排序、指令级并行重排序、内存系统重排序三个步骤对源代码进行指令重排,提高代码执行的性能。
但是在多线程情况下,操作系统“盲目” 地进行指令重排可能会导致我们不想看到的问题,如经典双检锁单例模式。那么我们就需要给操作系统定一些规矩或者上一些手段让他不要随意地指令重排。
但是由于操作系统和硬件的差异,内存访问的差异,会造成相同的代码运行在不同的系统上会出现各种问题。为了屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的多线程并发效果,所以有了 Java 内存模型(JMM)。
JMM 其实是一种抽象的概念,并不真实存在,它描述了一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
规范一:主内存和工作内存
主内存
主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的局部变量。由于是共享数据区域,多个线程同一个变量进行访问可能会发送线程安全问题。
工作内存
存储了主内存中的变量副本,每个线程不能访问其他线程的工作内存,因此存储在工作内存的数据不存在线程安全问题。在主内存中的实例对象可以被多线程共享,假如两个线程调用了同一个方法,那么两个线程会将要操作的数据拷贝一份到工作内存中,执行完再刷新到主内存。
规范二:数据同步八大原子操作
知道了工作内存和主内存是怎么交互的了,那么具体实现细节是什么呢?JMM 定义了八种操作来完成:
- lock(锁定):作用于主内存的变量,把一个变量标记为一个线程独占状态;
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
- read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以后随后的load工作使用;
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量;
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎;
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量;
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作;
- wirte(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量值传送到主内存的变量中。
如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行 read 和 load 操作;
如果把变量从工作内存中同步到主内存中,就需要按顺序地执行 store 和 write 操作。
规范三:保证多线程操作时具有原子性、可见性、有序性
1、原子性
含义: 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
解决方案: 使用 synchronized 和 lock 实现原子性,因为 synchronized 和 Lock 能够保证任一时刻只有一个线程访问该代码块。
2、可见性
含义: 当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
解决方案: 通过 synchronized、volatile 和 lock。
3、有序性
含义: 保证多线程间的语义一致。
解决方案: 通过 synchronized、volatile 和 lock。
规范四:as-if-serial 原则
不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。
规范五:happens-before 原则
只靠 synchronized 和 volatile 关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,从JDK 5 开始,Java 使用新的 JSR-133 内存模型,提供了 happens-before 原则来辅助保证程序执行的原子性、可见性和有序性的问题,它是判断数据是否存在竞争、线程十分安全的依据。happens-before 原则内容如下:
- 程序顺序原则,即在一个线程内必须保证语义串行,也就是说按照代码顺序执行。
- 锁规则,解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
- volatile规则, volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
- 线程启动规则,线程的 start() 方法先于它的每一个动作,即如果线程A在执行线程B的 start 方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见。
- 传递性,A先于B,B先于C,那么A必然先于C。
- 线程终止原则,线程的所有操作先于线程的终结,Thread.join() 方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回,线程B对共享变量的修改将对线程A可见。
- 线程中断规则,对线程 interrupt() 方法的调用先行发生于被中断线程的代码检查到中断事件的发生,可以通过 Thread.interrupted() 方法检测线程十分中断。
- 对象终结规则,对象的构造函数执行,结束先于 finalize() 方法。
volatile
volatile 的两个作用:
- 被 volatile 修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了被 volatile 修饰共享变量的值,新值总是可以被其他线程立即得知。
- 禁止指令重排序优化。
第一条是如何实现的?
当线程对 volatile 变量进行写操作时,JMM 会在写入这个变量之后插入一个 Store-Barrier(写屏障)指令,这个指令会强制将本地内存中的变量值刷新到主内存中。
当线程对 volatile 变量进行读操作时,JMM 会插入一个 Load-Barrier(读屏障)指令,这个指令会强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值。
第二条如何实现的?
在程序执行期间,为了提高性能,编译器和处理器会对指令进行重排序。但涉及到 volatile 变量时,它们必须遵循一定的规则:
- 写 volatile 变量的操作之前的操作不会被编译器重排序到写操作之后。
- 读 volatile 变量的操作之后的操作不会被编译器重排序到读操作之前。