Java并发在硬件层面的原理
再谈原子性:Java规范规定所有变量写操作都是原子的
Applications apps;
线程1:
apps = loadedApps;//原子的,不需要AtomicReference来处理
java语言规范里面,int i=0,resource=loadedResoures,flag=true,各种变量的简单的赋值操作,规定都是原子的
包括引用类型的变量的赋值写操作,也是原子的
你赋值的时候,要保证没有人先赋值过,没有人修改过,你才能赋值,AtomicReference的CAS操作来实现了,之前给大家讲解过的
但是很多复杂的一些操作,i++,先读取i的值,再更新i的值,i=y+2,先读取y的值,再更新i的值,这种复杂操作,不是简单赋值写,他是有计算的过程在里面的,此时java语言规范默认是不保证原子性的
volatile,保证的可见性和有序性,不保证原子性;i++,i=y+2,不是volatile可以保证原子性的
32位Java虚拟机中的long和double变量写操作为何不是原子的?
原子性这块,特例,32位虚拟机里的long/double类型的变量的简单赋值写操作,不是原子的,long i=30,double c=45.0,在32位虚拟机里就不是原子的,因为long和double是64位的
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
如果多个线程同时并发的执行long i=30,long是64位的,就会导致有的线程在修改i的高32位,有的线程在修改i的低32位,多线程并发给long类型的变量进行赋值操作,在32位的虚拟机下,是有问题的
就可能会导致多线程给long i=30赋值之后,导致i的值不是30,可能是-3333344429,乱码一样的数字,就是因为高低32位赋值错了,就导致二进制数字转换为十进制之后是一个很奇怪的数字
volatile原来还可以保证long和double变量写操作的原子性
volatile对原子性保障的语义,在java里很有限的,几乎可以忽略不计。32位的java虚拟机里面,对long/double变量的赋值写是不原子的,此时如果对变量加上了volatile,就可以保证在32位java虚拟机里面,对long/double变量的赋值写是原子的了
inti=0,原子性,volatile,java 语言规范就规定了,原子性的
volatile long i;
多个线程执行:i=30,此时就不要紧了,因为volatile修饰了,就可以保证这个赋值操作是原子的了
i++,复杂的一些场景
resources=loadResources();
resources.execute();
ready=true;
到底有哪些操作在Java规范中是不保证原子性的呢?
所有变量的简单赋值写操作,java语言规范原生给你保证原子性的;32位java虚拟机里的long/double是不保证赋值写的原子性的;volatile 可以解决这个问题;不保证原子性的一些操作呢?
i++
i=y+1
i=x*y==>先把x和y分别从主内存里加载到工作内存里面来,然后再从工作内存里加载出来执行计算(处理器),计算后的结果写回到工作内存里去,最后还要从工作内存里把i的最新的值刷回主内存
你敢说他是原子的?
volatile x=1;
volatile y=2;
volatilei=x*y;
我之前给大家已经说过了,画图都演示过了|
FSDirectory dir=...
synchonized(dir){
dir.add();
dir.remove();
dir.insert();
}
加锁
可见性涉及的底层硬件概念:寄存器、高速缓存、写缓冲器

从硬件级别来考虑一下可见性的问题
每个处理器都有自己的寄存器(register),所以多个处理器各自运行一个线程的时候,可能导致某个变量给放到寄存器里去,接着就会导致各个线程没法看到其他处理器寄存器里的变量的值修改了
可见性的第一个问题,首先,就有可能在寄存器的级别,导致变量副本的更新,无法让其他处理器看到
然后一个处理器运行的线程对变量的写操作都是针对写缓冲来的(store buffer)并不是直接更新主内存,所以很可能导致一个线程更新了变量,但是仅仅是在写缓冲区里罢了,没有更新到主内存里去
这个时候,其他处理器的线程是没法读到他的写缓冲区的变量值的,所以此时就是会有可见性的问题,这是第二个可见性发生的场景
然后即使这个时候一个处理器的线程更新了写缓冲区之后,将更新同步到了自己的高速缓存(cache)或者是主内存里,然后还把这个更新通知给了其他的处理器,但是其他处理器可能就是把这个更新放到无效队列里去,没有更新他的高速缓存
此时其他处理器的线程从高速缓存里读数据的时候,读到的还是过时的旧值
如果要实现可见性的话,其中一个方法就是通过MESI协议,这个MESI协议实际上有很多种不同的实现,因为他不过就是一个协议罢了,具体的实现机制要靠具体底层的系统如何实现
根据具体硬件的不同,MESI协议的实现是有区别的
比如说MESI协议有一种实现,就是一个处理器将另外一个处理器的高速缓存中的更新后的数据拿到自己的高速缓存中来更新一下,这样大家的缓存不就实现同步了,然后各个处理器的线程看到的数据就一样了
为了实现MESI协议,有两个配套的专业机制要给大家说一下:flush处理器缓存、refresh处理器缓存。
flush处理器缓存,他的意思就是把自己更新的值刷新到高速缓存里去(或者是主内存),因为必须要刷到高速缓存(或者是主内存)里,才有可能在后续通过一些特殊的机制让其他的处理器从自己的高速缓存(或者是主内存)里读取到更新的值refresh处理器缓存,他的意思就是说,处理器中的线程在读取一个变量的值的时候,如果发现其他处理器的线程更新了变量的值,必须从其他处理器的高速缓存(或者是主内存)里,读取这个最新的值,更新到自己的高速缓存中
所以说,为了保证可见性,在底层是通过MESI协议、flush 处理器缓存和refresh 处理器缓存,这一整套机制来保障的
之前给大家讲过那个volatile关键字的作用,对一个变量加了volatile修饰之后,对这个变量的写操作,会执行flush处理器缓存,把数据刷到高速缓存(或者是主内存)中,然后对这个变量的读操作,会执行refresh处理器缓存,从其他处理器的高速缓存(或者是主内存)中,读取最新的值
深入探秘有序性:Java程序运行过程中发生指令重排的几个地方

我们写好的代码在实际执行的时候那个顺序可能在很多环节都会被人给重排序,一旦重排序之后,在多线程并发的场景下,就有可能会出现一些问题
(1)自己写的源代码中的执行顺序:这个是我们自己写的代码,一般来说就是按照我们自己脑子里想的样子来写
(2)编译后的代码的执行顺序:java 里有两种编译器,一个是静态编译器(javac),一个是动态编译器(JIT)。javac负责把.java文件中的源代码编译为.class文件中的字节码,这个一般是程序写好之后进行编译的。JIT负责把.class文件中的字节码编译为JVM所在操作系统支持的机器码,一般在程序运行过程中进行编译。
在这个编译的过程中,编译器是很有可能调整代码的执行顺序的
(3)处理器的执行顺序:哪怕你给处理器一个代码的执行顺序,但是处理器还是可能会重排代码,更换一种执行顺序
(4)内存重排序:有可能你这个处理器在实际执行指令的过程中,在高速缓存和写缓冲器、无效队列等等,硬件层面组件,也可能会导致你的指令的执行顺序跟想象的不大一样
上述就是在我们写好java代码之后,从编译到执行的过程中,代码的执行顺序可能会有指令重排的地方,只要有指令重排就有一定可能造成程序执行异常
但是编译器和处理器不是胡乱的重排序的,他们会遵循一个关键的规则,就是数据依赖规则,如果说一个变量的结果依赖于之前的代码执行结果,那么就不能随意进行重排序,要遵循数据的依赖比如说:
int a=3;
int b=5;
int c=a*b;
那第三行代码依赖于上面两行代码,第一行和第二行代码可以重排序,但是第三行代码必须放在最下面
此外,之前给大家介绍过happens-before原则,就是有一些基本的规则是要遵守的,不会让你胡乱的重排刷
JIT编译器对创建对象的指令重排以及double_check单例实践
略

浙公网安备 33010602011771号