Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

主内存与工作内存

处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。

图片.png

所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。

图片.png

内存模型三大特性

1. 原子性(Atomicity)

原子性是指操作是不可分的,要么全部一起执行,要么不执行。在java中,其表现在对于共享变量的某些操作,是不可分的,必须连续的完成。比如a++,对于共享变量a的操作,实际上会执行3个步骤:
1.读取变量a的值,假如a=1
2.a的值+1,为2
3.将2值赋值给变量a,此时a的值应该为2
这三个操作中任意一个操作,a的值如果被其他线程篡改了,那么都会出现我们不希望出现的结果。所以必须保证这3个操作是原子性的,在操作a++的过程中,其他线程不会改变a的值,如果在上面的过程中出现其他线程修改了a的值,在满足原子性的原则下,上面的操作应该失败。
java中实现原子操作的方法大致有2种:锁机制无锁CAS机制

2. 可见性

可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。

  • 我们定义的所有变量都储存在 主内存
  • 每个线程都有自己 独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  • 线程对共享变量所有的操作都必须在自己的工作内存中进行,不能直接从主内存中读写(不能越级)
  • 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行。(同级不能相互访问)

Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
主要有三种实现可见性的方式:

  • volatile
  • synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
  • final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。

3. 有序性

有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

  1. int a = 1; //1
  2. int b = 20; //2
  3. int c = a + b; //3

编译器优化后可能变成

  1. int b = 20; //1
  2. int a = 1; //2
  3. int c = a + b; //3

volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

单例模式的双重校验

  1. public class Singleton {
  2. static Singleton instance;
  3. static Singleton getInstance(){
  4. if (instance == null) {
  5. synchronized(Singleton.class) {
  6. if (instance == null)
  7. instance = new Singleton();
  8. }
  9. }
  10. return instance;
  11. }
  12. }

存在的问题:instance是没有初始化的,此时去使用instance可能会产生一些意想不到的错误。
现在比较好的做法就是采用静态内部内的方式实现:

  1. public class SingletonDemo {
  2. private SingletonDemo() {
  3. }
  4. private static class SingletonDemoHandler{
  5. private static SingletonDemo instance = new SingletonDemo();
  6. }
  7. public static SingletonDemo getInstance() {
  8. return SingletonDemoHandler.instance;
  9. }
  10. }

Happens-Before (先行发生)原则

Happens-Before 规则最初是在一篇叫做Time, Clocks, and the Ordering of Events in a Distributed System的论文中提出来的,在这篇论文中,Happens-Before 的语义是一种因果关系。在现实世界里,如果 A 事件是导致 B 事件的起因,那么 A 事件一定是先于(Happens-Before)B 事件发生的,这个就是 Happens-Before 语义的现实理解。
在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。

1. 单一线程原则

Single Thread rule

在一个线程内,在程序前面的操作先行发生于后面的操作。

图片.png

2. 管程锁定规则

管程锁定规则Monitor Lock Rule:管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。管程中的锁在 Java 里是隐式实现的,例如下面的代码,在进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮我们实现的。
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。

图片.png

3. volatile 变量规则

Volatile Variable Rule

对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。

图片.png

4. 传递性

Transitivity

如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。

5. 线程启动规则

线程启动规则Thread Start Rule:它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

图片.png

6. 线程加入规则

线程加入规则Thread Join Rule:Thread 对象的结束先行发生于 join() 方法返回。

图片.png

7. 线程中断规则

Thread Interruption Rule

对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。

8. 对象终结规则

Finalizer Rule

一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。