并发的由来
- JAVA内存模型规定了所有变量第一存储在主内存中,每个线程都有自己的工作内存。
- 每个线程都保存该线程用到的变量的主内存副本,线程操作的都是自己工作内存,而不是主内存
- 线程访问变量过程为从主内存拷贝一份到工作内存,操作变量不会马上同步到主内存,由JMM控制
- 线程间无法直接操作其他线程的工作内存,共享变量的传递需要靠工作内存与主内存传递的方式同步
volatile关键字
保证可见性,不保证原子性
通过javap -v 查看可知,JVM通过添加Lock汇编指令实现可见性
可见性原理
- CPU、内存、IO处理速度不一致
- JMM
由于处理速度的不一致,为了缓解这个问题,引入了CPU高速缓存处理内存处理IO速度的差异,将内存中的数据复制到高速缓存中。多核处理器中会出现情况,CPU0、CPU1缓存了内存数据,CPU0修改了数据同步到缓存中,CPU1还未同步到缓存中,出现缓存不一致,而缓存不一致导致数据操作会有问题。CPU层面为了解决这个问题,引入了总线锁(串行读取,性能降低)和缓存锁。
缓存锁则利用缓存一致性协议(MESI)实现。对缓存的数据标记状态(Modify修改,Invalid失效,shared一致,exclusive独占,只有一个cpu有值),cpu通过嗅探协议得到其他cpu缓存数据的状态,实现数据的一致性。
但是MESI是指在硬件层面上解决缓存一致性问题,cpu在发起数据修改时需要通知其他cpu并等待响应,于是优化引入storebuffer实现异步响应提高处理能力。
而引入storebuffer就带来了可见性问题。并且CPU执行还可能出现重排序问题。
image.png
为了解决这个问题,引入内存屏障。
内存屏障
变量写的过程包括三个指令:
load:从主内存读取数据到工作内存
update:修改变量值
store:刷新到主内存
这个过程如果线程A读取了变量I值(10),自增i++(11),此时线程B也读取了变量I值(10),也自增了I++(11)写入主内存,此时线程A强制刷新变量I为B的I++(11)的值,但是工作内存中的I还是11,然后刷新到主内存,导致结果不是想要的(12)。内存屏障前的指令是可以被从排序的,导致线程安全问题。内存屏障的作用是强制刷新到主内存,禁止指令重排序。
内存屏障的作用:
- 当修改一个变量时,保证把值刷新到主内存
- 其他线程用到修改的变量,会把自己工作内存中的变量标记为无效,从主内存读取。
- 使用场景:对于一个变量只有一个线程会对变量进行写操作,其他线程都是读操作,则可以使用volatile修饰。
JMM
- 语言级别的抽象内存模型
- 控制如何做数据同步及什么时候做数据同步
- 核心解决了可见性、有序性
- 通过调用CPU只能实现一致性
JAVA代码的执行顺序经过编译器、CPU的重排序,出现一些不可预知的执行顺序。
指令重排序的规则
- 操作系统、JVM编译器为了优化程序处理的效率,会对操作指令进行调度,重新排序。但必须遵守以下规则
- 存在依赖关系的指令不重排
- 不影响单线程的执行结果
volatile原理
JVM的volatile使用内存屏障实现
内存屏障的作用
- 确保指令重排序时,屏障前面的指令执行完,保证其后的指令不会重排到屏障之前,不会把屏障之前的指令重排序到屏障之后
- 强制刷新工作内存缓存到主内存;
- 工作内存变量的写操作会导致其他线程的缓存失效(MESI),其他线程的读操作从主内存读
JVM中提供了四种内存屏障指令
- loadload
- storestore
- loadstore
- storeload
volatile通过storeload(编译器级别的),JVM会调用操作系统底层的指令实现内存屏障来实现的禁止指令重排序。
局限性
只保证可见性,不保证原子性。适合只有一个线程修改变量,其他线程读取变量的场景。
happen-before原则实现可见性
- 程序的运行顺序规则
例如代码,在单线程环境下,1 happen-before 2,3 happen-before 4
public class App {
private static int x = 0;
public static volatile boolean isfinish = false;
public void write() {
x = 1; // 1
isfinish = true; //2
}
public void read() {
if (!isfinish) { // 3
int a = x; // 4
}
}
- volatile规则
对于上面的代码因为isfinish加了volatile修饰,则2 happen-before 3
- 传递性规则
因为1 hp 2 ,2 hp 3,则传递性规则保证 1 hp 3
- start规则
线程启动前的命令必然 hp 线程运行执行的命令(操作同一个变量)
- join原则
线程运行执行的代码必然 hp thread.join()命令。join实现原理是wait/notify,通过阻塞主线程,执行完成notifyAll在唤醒继续执行。
- 锁规则
前一个线程释放锁必然 hp 后一个线程加锁操作。
网友评论