内容学习自:https://www.cnblogs.com/dolphin0520/p/3920373.html
每核cpu在执行指令时,都会将数据从主存中读取,然后在自身的高速缓存中操作修改,最后刷新到主存中.
但有个问题,如果此时主存有个变量
i = 0;
两个cpu线程紧挨着先后执行下面的代码;
i = i + 1;
可能会出现这个情况:线程1和线程 2都读取了i= 0 到自身的高速缓存中.线程1执行+1操作,写入到内存.线程2也执行+1操作写入内存.最终i = 1;
这就是缓存一致性的问题,有两个解决方案:
1.总线锁机制.操作共享资源都加锁.好吧,这效率就甭说了.
2.缓存一致性协议,最出名的是Intel的MESI,意思是说,当cpu写完数据后,会通知其他cpu,去查看其高速缓存中是否有该变量的副本,如果有的话,请设置为无效,然后重新去主存中读取.
那么上面的情况就变成了.
线程1和线程 2都读取了i= 0 到自身的高速缓存中.线程1执行+1操作,写入到内存,通知线程2.线程2发现了有i=0的副本,然后作废,从内存中读取i=1,然后执行+1操作.
原子性:经典的例子就是转账,不能中断.
可见性:共享的变量某线程修改后,立即刷新主存,那么这个改变对其他线程来说,便是可见的.
有序性:处理器出于优化的机制会对代码指令重排,但并不影响结果.
比如
int a = 1 ;//指令1
int b = 2 ;//指令2
int c = a + b;//指令3
那么处理器真正执行的
可能是:指令1 ---指令2 ----指令3,
也可能是:指令2---指令1---指令3
但绝不会先执行指令3 ,因为指令3具有依赖性.
这对于单线程来说,没什么问题.可是对于多线程来说呢?
博主也不清楚.
java有自己的一套内存模型,并定义了执行顺序,但是出于效率考虑,java并没有限制物理机的一些提升操作.也即是java也存在着缓存一致性和指令重排序问题.
java内存模型规定:所有变量存在于虚拟机内的主存(类似于机器主存),每条线程都有一个工作内存(类似于cpu高速缓存).变量的更新都必须在工作内存中执行,然后刷新到主存.不能直接在主存中更新.
java只保证基本数据类型的读取和赋值是原子性的.
int a = 1;//赋值10给a
int b = a;//读取a,赋值a给b
a ++;//读取a,赋值a+1给a
以上三个只有第一行是原子性的.其他都不是.
当然synchronized和Lock是保证代码片的执行时原子性的.
volatile可以保证可见性,被volatile修饰的共享变量修改后会立即刷新到主存.
当然synchronized和Lock算是只允许单线程操作,对单线程来说,讨论可见性是没有意义的.
volatile也可以保证有序性.在后面说.
当然synchronized和Lock算是只允许单线程操作,对单线程来说,讨论有序性是没有意义的.因为不影响结果.
实质上,java内部就对执行顺序有个规则,叫做happens-before原则(先行发生原则):
解读部分:
规则1:几乎是所有编程的规则.自上而下从左至右.但依然有可能指令重排.
规则2:解锁发生在另一个线程的开锁之前.
规则3:对一个变量的写操作先行发生于后面对这个变量的读操作.非常精简.这个在后面会说.
volatile的作用:
1.保证了不同线程对这个变量进行操作时的可见性,一个线程修改后,其他线程都知道.
2.禁止指令重排.
第一条它保证了可见性.
第二条的意思:
当执行到volatile时,前面的代码必须都执行完了.volatile后面的代码必须都未执行.不管是否重排.
int a = 1;
int b = 2;
boolean flag = true;//volatile
int c = 3;
int d = 4;
这段代码.执行到flag时,c和d都没有执行.
而且指令重排时,a和b可以互换,c和d可以互换,但是无法越界.a和c无法互换.
volatile就是一道栅栏.它可以保证一定的有序性.
但是不能保证原子性.
为什么?
假如两条线程都去操作下面代码片:
a++;
那么a++本身是 读取和更新两个步骤.
还记得java执行规则的第三条吗?volatile可以保证读取时在更新之前,但不能保证读取是在读取之前.
比如线程1读取了a,此时在更新状态.那么线程2就不能读取,因为线程1还没有更新完到主存.
但是如果线程1读取了a,还没有更新,那么线程2就可以读取,
并发情况下,volatile比synchronized更有效率,但因不具备原子性,所以不能代替synchronized.个人觉得场景可以在/缓存/更新等地使用.记得wx-tools的获取access_token就使用了这个关键字.