一.什么是缓存一致性问题?应该怎么解决?
为了解决处理器和内存读写速度相差很大的问题,在它们之间加入了一个缓冲:高速缓存;
运算过程:
当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存中,那么CPU进行运算时就可以直接从他的高速缓存读取数据和向其中写入数据,当运算结束后,再将高速缓存中的数据刷新到主内存中。
缓存一致性问题:
在多处理器系统中,每个处理器都有自己的高速缓存;
而这些高速缓存又共享同一主内存;
当多个处理器的运算任务都涉及到同一块主存区域时,将会导致各自的缓冲数据不一致;
那么同步回主内存的数据也就不确定,这就是缓存一致性问题。
缓存一致性问题解决方法:
1.通过在总线加上Lock锁的方式
CPU和其他部件(例如主存)的通信是通过总线的方式来进行的,对总线加LOCK锁就是说阻塞了其他CPU对其他部件(主存)的访问;
缺点:在锁住总线期间,其他CPU无法访问内存,导致效率低下
2.通过缓存一致性协议
该协议保证了每个缓存中使用的共享变量的副本是一致的。
核心思想是:当CPU向内存写入数据时,如果发现操作的变量是共享变量,会发出信号通知其他CPU将该变量的缓存行变为无效状态,那么当其他CPU想要使用这个变量时,发现它是无效的,会重新从内存中读取。
二.java内存模型
1.内存模型的定义:
在特定的操作协议下,对特定的内存和高速缓存进行读写访问的过程的抽象。
2.java内存模型(jmm):
解决的是在虚拟机中如何将变量存储到内存和从内存中取出变量的底层细节过程。
java内存模型规定所有的变量都存储在主内存(类比主内存)中;
每个java线程还有自己的工作内存(类比高速缓存);
java线程对变量的操作都必须在工作内存中进行,而不能直接对主存进行操作;
不同线程之间不能直接访问对方工作内存中的变量;
线程间变量值的传递均需要通过主内存来完成。
三.volatile关键字
volatile是java虚拟机提供的最轻量级的同步机制。
当一个变量定义为volatile之后,就具备了两种特性:
1.保证此变量对所有线程的可见性,也就是说当一个线程修改了这个值,新的值对于其他线程来说是可以立即得知的;
第一,使用volatile关键字会强制将修改的值立即写入主存;
第二,使用volatile关键字的话,当线程2进行变量修改时,会使得线程1的缓存行无效;
第三,当线程1想要使用这个变量时,发现缓存行无效,回去主存重新读取。
2.禁止进行指令重排序优化
第一层意思:当程序执行到volatile变量的读操作或者是写操作时,在其前面的操作的修改肯定已经全部进行,并且结果对后面可见;在其后面的操作肯定还没有进行;
第二层意思:在进行指令优化的时候,不能吧volatile变量前面的语句放到后面执行,也不能吧volatile变量后边的语句放到前面执行。(指令重排序会考虑指令之间的数据依赖性,无法越过内存屏障(lock前缀指令))。
内存屏障的作用:
1.先于这个内存屏障的指令必须先执行,后于这个内存屏障的指令必须后执行。
2.使得内存可见性
应用:在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据;在写指令后插入写屏障,能让写入缓存的最新数据写回到内存。
volatile应用场景:DCL(双重校验锁)
四.原子性,可见性,有序性
保证并发程序正确运行,必须保证原子性,可见性,有序性。
1.原子性:
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么全部不执行。
可以通过Synchronized(同步块)或者Lock实现原子性。
例题: 请分析以下哪些操作是原子性操作。
x = 10; //语句 1
y = x; //语句 2
x++; //语句 3
x = x + 1; //语句 4
特别注意,在 在 java 中,只有对除 long 和 和 double 外的基本类型进行简单的赋值(如 int a=1 )或读取操作,才是原子的 。只要给 long 或 double加上 volatile ,操作就是原子的了。
语句 1 是原子性操作;
语句 2 实际上包含 2 个操作,它先要去读取 x 的值,再将 x 的值写入工作内存,虽然读取 x 的值以及将 x 的值写入工作内存这 2 个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1 包括 3 个操作:读取 x 的值,进行加 1 操作,写入新的值。
2. 可见性:
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即得知这个修改。
通过volatile(变化的值立即同步到内存) synchronized(对一个变量执行unlock之前,必须先把此变量同步会回主内存中) final
3.有序性:
有序性:程序执行的顺序按照按照代码的先后顺序执行。
在本线程中观察,所有的操作都是有序的(按照代码先后顺序执行);在一个线程中观察另一个线程,所有的操作都是无序的(不存在数据依赖性的指令重排序和工作内存和主内存同步延迟思想)
通过synchronized(一个变量在某一时刻只允许一条线程对其进行lock操作)(持有同一个锁的两个同步块有序)
五.先行发生原则
先行发生是java内存模型中定义的两项操作之间的偏序关系。
1.程序次序规则:在一个线程里面,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2.管程锁定规则:一个unlock操作先行发生于后面对一个锁的lock操作。
3.volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作;
4.线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作;
5.线程终止规则:线程中的所有操作都先行发生于此线程的终止检测(Thread.join方法结束,Thread.isAlive返回值检测到线程终止)
6.线程中断规则:对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生(Thread.interrupted(检测是否有中断发生))
7.对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize方法开始;
8.传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,就可以得出操作A先行发生于操作C.