内存模型基本概念
在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步。
线程之间的通信机制有两种:共享内存和消息传递。
共享内存:线程之间共享程序的公共状态,通过写-读内存的公共状态进行隐式通信。
消息传递: 线程之间没有公共状态,线程之间必须通过发送消息来进行通信。(类似消息队列)
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
并发编程中有3大重要特性
原子性 - 保证指令不会受线程上下文切换的影响
可见性 - 保证指令不受cpu缓存的影响
有序性 - 保证指令不会受cpu指令并行优化的影响
Java虚拟机规范中定义了一种Java内存 模型(Java Memory Model,即JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。Java内存模型的主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节。
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
-
首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
-
然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
可见性
退不出的循环
/**
* @Author blackcat
* @version: 1.0
* @description:可见性(用不停止的 可以加volatile 从主存中获取变量)
* volatile适用仅用在一个写线程,多个读线程的情况
*synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是
*synchronized 是属于重量级操作,性能相对更低
*/
@Slf4j
public class Threadvisibility {
private static boolean flag = true;
public static void main(String[] agrs) {
new Thread(() -> {
log.info("start");
while (flag) {
}
log.info("end");
}, "server").start();
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("修改flag");
flag = false;
}
}
volatile 可以用来修饰成员变量和静态成员变量,变量具有以下特性
可见性::对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile i++这种复合操作不具有原子性。
volatile适用仅用在一个写线程,多个读线程的情况。
volatile 原理
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
对 volatile 变量的写指令后会加入写屏障
对 volatile 变量的读指令前会加入读屏障
如何保证可见性
写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见
读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。
import lombok.extern.slf4j.Slf4j;
/**
* @Author blackcat
* @create 2021/8/1 20:38
* @version: 1.0
* @description:
*/
@Slf4j
public class VolatileTest {
public static void main(String[] args) {
VolatileExample volatileExample = new VolatileExample();
new Thread(() -> {
volatileExample.writer();
}, "write").start();
new Thread(() -> {
volatileExample.reader();
}, "read").start();
}
}
@Slf4j
class VolatileExample {
int a = 0;
volatile boolean flag = false;
public VolatileExample() {
}
public void writer() {
log.info("writer start");
a = 1;
flag = true;
log.info("writer end");
}
public void reader() {
log.info("before{}",a);
if (flag) {
log.info("after{}",a);
}
}
}
如何保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。
有序性
定义
指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.
CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。
a = b + c;
d = e - f ;
先加载b、c(注意,即有可能先加载b,也有可能先加载c),但是在执行add(b,c)的时候,需要等待b、c装载结束才能继续执行,也就是增加了停顿,那么后面的指令也会依次有停顿,这降低了计算机的执行效率。
为了减少这个停顿,我们可以先加载e和f,然后再去加载add(b,c),这样做对程序(串行)是没有影响的,但却减少了停顿。既然add(b,c)需要停顿,那还不如去做一些有意义的事情。
综上所述,指令重排对于提高CPU处理性能十分必要。虽然由此带来了乱序的问题,但是这点牺牲是值得的。
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。
//执行次数比较多,指令重排 (0,0)
public class Disorder {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
reSort();
}
}
static void reSort() throws InterruptedException {
Thread one = new Thread(new Runnable() {
public void run() {
a = 1; //操作1
x = b; //操作2
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1; //操作3
y = a; //操作4
}
});
one.start();
other.start();
one.join();
other.join();
if (x == 0 && y == 0) {
System.out.println("(" + x + "," + y + ")");
}
}
}
DoubleCheckedLocking问题
/**
* @Author blackcat
* @version: 1.0
* @description:双重检查
*/
public class DoubleCheckedLocking { //1
//加入volitale
private static Instance instance; //2
/**
* instance = new Instance();
* 可分解为3行伪代码
* 1、分配对象的内存空间
* 2、初始化对象
* 3、设置instance指向刚分配的内存地址
*
* 2与3的操作存在重排序
* 当多线程时候会判断成instance == null 直接返回了对象但是对象还没有初始化
*/
public static Instance getInstance() { //3
if (instance == null) { //4:第一次检查
synchronized (DoubleCheckedLocking.class) { //5:加锁
if (instance == null) //6:第二次检查
instance = new Instance(); //7:问题的根源出在这里
} //8
} //9
return instance; //10
} //11
static class Instance {
}
}
happen-before 规则
- 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作、
- join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。