1 什么是可见性?
- 通过
volatile
修饰的变量被a线程修改b线程能立即读取到修改后的值,不会出现'脏读'
2 可见性原理
-
volatile
修饰后hsdis多了个Lock
汇编指令,Lock
汇编指令是一种控制指令,作用是在多线程环境中,可以基于总线锁或缓存锁的机制来达到共享变量在线程间的可见性
3 硬件层面
- CPU>内存>IO 硬件方面存在很大的处理速度的差异,木桶原理最---最短板决定整体性能
- 所以硬件方面的性能优化要从两方面着手:
①提高短板(基本不可实现)
②最大化利用【性能过剩组件(CPU)】
3.1 最大化利用CPU方法
image.png- CPU增加高速缓存,cpu绝大多数的业务处理中都会依赖内存或者IO进行运算或数据存储
- CPU告诉缓存通过降低内存/IO读取频率来实现提高整体处理性能
- CPU高速缓存分为:L1>L2>L3三种,性能依次下降
- L1d:L1数据缓存
- L1i:L1指令缓存
- CPU高速缓存提高了CPU处理过程中频繁与主内存交互的性能
- CPU高速缓存也带来了缓存(数据)一致性的问题
3.2 缓存(数据)一致性解决方案:
- 总线锁
通过在总线添加锁的方式来保证缓存(数据)一致性,当cup0通过总线操作数据时,其它cpu1将无法获取总线的使用权限,对性能影响很大 - 缓存锁
相对于总线锁缓存锁的范围更加精确,降低看控制粒度,通过缓存一致性协议实现 - 缓存一致性协议MESI
不同的CPU架构里缓存一致性协议有着各自不同的实现方式,X86架构中是基于MESI协议
image.png
M>Modified 修改状态
E>Exclusive 独享状态
S>Shared 共享状态:表示数据可被多个缓存对象进行缓存,且数据值与主内存一致
I>Invlid 失效状态
失效状态缓存不可被使用,将从主内存中进行读取
3.3 MESI的局限性
-
当某个CPU修改缓存中的数据时,首先通知其cup缓存中的相同数据,其它相同缓存置为失效
其它CPU缓存失效完成后再通知要修改的CPU,该过程中CUP处于阻塞中,浪费了CPU性能
image.png
3.4 EMSI 改进
-
为了减少缓存被修改过程中的阻塞时长,通知修改时采用异步操作,不进行阻塞
将修改请求缓存到storebuffer中
image.png - storebuffer带来的问题
value =3;
void cup0{
value = 10;// 通过storebuffer异步通知其他cpu缓存,将缓存value变为I:失效状态
isFinish = true; //E 独占状态
}
void cup1{
//由于cup0中storebuffer是异步操作
//所以理论上村 isFinish=true 而 value=3 这种情况
if(isFinish){//true
assert value == 10;//false
}
}
storebuffer可能会导致cup的乱序执行既"指令重排序",重排序将带来可见性问题
- 硬件层面的优化,总是会带来其他问题,无法真正解决可见性问题,所以cpu层面提供指令--内存屏障供软件方面调用
3.5 内存屏障
value =3;
void cup0{
value = 10;// 通过storebuffer异步通知其他cpu缓存,将缓存value变为I:失效状态
加入内存屏障
isFinish = true; //E 独占状态
}
void cup1{
//由于cup0中storebuffer是异步操作
//所以理论上村 isFinish=true 而 value=3 这种情况
if(isFinish){//true
读取内存屏障//由于cup0在 'sFinish = true; //E 独占状态' 前加入内存屏障
//所以下面代码中value值将,直接从主内存中进行获取
assert value == 10;//false
}
}
-
cup层面提供了3中内存屏障
读屏障 store barrier
写屏障 load barrier
全屏障 full barrier -
X86架构中volatile关键字的实现依赖:volatile--->Lock指令(缓存锁)--->内存屏障
-
内存屏障/指令重排序 等和平台一级硬件有关,不同硬件是不同的实现.java是跨平台语言,不需要在业务点中考虑硬件的差异性的是依托于JMM内存模型的存在
4 JMM虚拟内存模型
image.png- 语言基本的抽象内存模型,本与cpu内存模型相类似
- 线程通过操作工作内存来修改数据,工作内存负责和主内存进行通信和数据同步
- JMM虚拟内存模型为作为一种标准,不同的硬件设备有着各自的实现(指令).通过JMM业务代码开发人员不需要关系硬件差异化,从而实现语言的跨平台
4.1 重排序
- 代码重排序顺序:源代码->编译器重排序->CPU层面重排序(指令级、内存)->最终执行的指令
- 通过重排序可以提高代码效率,但不是所用情况都会进行重排序,是否重排序取决于【数据依赖规则】
/**
* 无数据依赖
* 1&2行代码间无相互依赖
* 可进行从排序
*/
int a = 1;
int b = 2;
/**
* 部分数据依赖
* 1&2 行代码间无数据依赖
* 1&3 行代码间存在数据依赖
* 2&3 行代码间存在数据依赖
* 1&2行可进行重排序 1&3 2&3 行不可重排序
*/
int a=1;
int b = 2;
int c = a+b;
-
数据依赖规则:as-if-serial
无论代码以何种方案进行重排序,对于单个线程执行代码的结果不可变 -
Happens-Befor
代码A代码的执行结果对于B代码必须是可见,就成为 A Happens-Befor B -
那些场景会触发Happens-Before规则?
① 【程序的顺序规则】
/**
* 单线程调用该方法时,A Happens-Befor B
**/
function X(){
a =1;// A
b =2;// A
}
② 【volatile规则】
被volatile
修饰的变量写操作一定对读操作可见,即 "写" Happens-Befor "读"
③【传递性规则】
如果 :A Happens-Befor B
& B Happens-Befor C
那么: A Happens-Befor C
④ 【start规则】
主线程里的start()
方法 Happens-Befo 该线程run方法内任意代码
/**
* B Happens-Befor C (start规则)
* A Happens-Befor B (顺序规则)
* A Happens-Befor C (传递性规则)
**/
public class A{
static x=0;
public static void main(String []args){
Thread t1=new Thread(()->{
//C .....
});
x=10;//A
t1.start();//B
}
}
⑤【Join规则】
线程run方法内代码 Happens-Befor join()
后的代码
public class Demo {
static int a = 0;
/**
* A Happens-Befor B
**/
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
a = 99;//A
});
t1.start();
t1.join();
System.out.println(a);//B
}
}
⑥ 【synchronized监视器锁规则】
synchronized
的占用顺序决定线程代码顺序
public class Demo {
public void xx(){
synchronized (this){
//A...
}
}
public static void main(String[] args) {
/**
*t1 线程t1 代码A Happens-Befor 线程t2 代码A
*
**/
Demo demo = new Demo();
Thread t1 = new Thread(()-> demo.xx());
Thread t2 = new Thread(()-> demo.xx());
t1.start();
t1.join();//保证t1先于t2
t2.start();
}
}
- 无数据依赖情况下禁止重排序
当代码间不存在数据依赖,但在多线程调用的场景下可能会导致执行结果错误,此时需要人工干预重排序---JMM内存屏障
value =3;
void cup0{
value = 10;// 通过storebuffer异步通知其他cpu缓存,将缓存value变为I:失效状态
isFinish = true; //E 独占状态
}
void cup1{
if(isFinish){//true
assert value == 10;//false
}
}
- JMM内存屏障:编译器级别内存屏障、CPU级别内存屏障
4.2 JMM解决有序性、可见性方案
- volatile
可解决可见性。通过内存屏障实现 - synchronized
可解决可见性、有序性、原子性。通过对线程阻塞实现单线程调用来实现 - final
遍历不可变,避免了可见性、原子性等问题 - happens-before
5 线程的顺序执行
- 使用join
阻塞主线程,直到调用join()
方法的线程执行完毕;或者说调用join()
线程的执行结果对主线程可见,底层通过wait/notify实现
/**
* 只有添加join后线程才会123依次执行
**/
Thread t1 = new Thread(()->{
//doSomething1
});
Thread t2 = new Thread(()->{
//doSomething2
});
Thread t3 = new Thread(()->{
//doSomething3
});
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
网友评论