一、介绍

- 线程通信

1. 共享内存 消息传递

- 线程同步

- 内存模型的抽象

实例域,静态域和数据元素存储在内存中,堆内存在线程之间共享,但是局部变量,方法定义参数和异常处理参数不会在线程之间共享。JAVA线程之间的通信由JAVA内存模型(JMM)控制,它可以决定一个线程对共享变量的写入何时对另一个线程可见,即JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本~本地内存是一个抽象概念,而不是真实存在~


java 线程变量共享 java线程共享内存_执行顺序

A/B写进主内存,B/A从主内存读取,实质上是通过两个进程之间的消息传递(当A与B需要通信,修改线程会先将结果刷到内存中)

- 重排序,为了执行程序时提高性能,编译器和处理器常常会对指令做重排序,三种类型:

1. 编译器优化的重排序,编译器在不改变单线程线程语义的前提下,可以重新安排语句的执行顺序(会禁止某种特定的编译器排序)

2. 处理器重排序,指令以及并行的重排序,将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序,在java编译器生成指令序列的时插入特定类型的内存屏障memory barriers,通过它来限制特定类型的处理器重排序

3. 处理器重排序,内存系统的重排序,由于处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去可能是乱序执行的

java 线程变量共享 java线程共享内存_java 线程变量共享_02

- 写缓冲区的优点

批写入节省时间,合并写操作,避免对同一个内存地址多次写,减少对内存总线的占用,而且对于回滚也是有好处的。但是,写缓冲区只对它所在的处理器可见,处理器的执行顺序,不一定和内存实际发生的顺序一致(就是当写入的数据还未被刷入主内存中,主内存中的旧数值被读取)

- 重排序需要考虑因素(都是针对单线程)

1. 数据依赖性,针对单个处理器中执行的质量顺序,不同处理器之间和不同线程之间的数据依赖性不被考虑

举例:

(1)a = 1;

(2)b = a;

第二句依赖于第一句,所以不能被重排序

2. as-if-serial,不管怎么排序,程序的执行结果不能被改变~

举例:

(1)double pi = 3.14;
(2)int a = 3;
(3)double area = a * pi;

然而调换第一句和第二句的位置并不影响程序的执行结果

- happen-before关系,JMM中,仅仅要求前一个操作的执行结果对后面一个操作可见即可

1. 程序顺序规则,一个线程中中的每个操作

2. 监视器锁规则

3. volatile变量规则

4. 传递性

二、一致性内存模型

- 一个线程中的所有操作必须按照程序的顺序来执行

- 所有线程都只能看到一个单一的操作执行顺序(就是不同时刻不同的线程执行时,看见的是一样的操作执行顺序)

1. JMM表示如果是正确同步的,程序的执行将具有一致性,而未同步的程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致

2. 顺序一致性模型中,所有操作完全按程序的顺序串行执行,而在JMM中,临界区内的代码可以重排序(JMM不允许临界区内的代码“溢出”临界区之外,里面的语句必须在进入临界区和退出临界区之间发生)

class SynchronizedExample {
int a = 0;
boolean flag = false;
public synchronized void writer() {
a = 1;
flag = true;
}
public synchronized void reader() {
if(flag) {
int i = a;
}
} 
}

3. JMM不保证对64位的long位和double型变量的读/写操作具有原子性

总线事务:处理器与内存之间的数据传递,包括读事务和写事务。一个处理器执行总线事务期间,总线会禁止其他所有的处理器和I/O设备执行内存的读/写~

在一个32位的处理器上,如果要求对64位数据的写操作具有原子性,会有比较大的开销。因为一般是把一个64位数据拆分成两个32位写操作来执行,而这两个写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的写将不具原子性


java 线程变量共享 java线程共享内存_重排序_03

4. 控制依赖关系

block的执行速度,编译器和处理器会采用“猜测执行”,对于单线程并不会改变执行结果,而对于多线程,语义就被破坏了,需要我们的同步

Reference:

1. 深入理解JAVA内存模型