一、首先要了解Java的内存模型JMM
此部分参考volatile和synchronized到底啥区别?
①为了解决内存和CPU之间速度的差异,Java 添加了多个缓存:
如图,在cpu和主内存之间,添加了两级缓存
②对应到具体线程中:
每个线程都有自己的L1缓存,然后共用一个L2缓存,最后才是主内存
共享变量将在L1、L2和主内存中都存在,这样当L1中存在该变量就不必去主内存寻找,补上了内存速度慢的短板。
③线程读/写共享变量时的步骤:
a. 从主内存复制共享变量到自己的工作内存
b. 在工作内存中对变量进行处理
c. 处理完后,将变量值更新回主内存
二、共享变量内存不可见问题
根据上面的缓存模型,可以发现一些共享变量的问题。
例如:
在主内存中有变量 X=0
线程1和线程2先后访问该变量
线程1先访问:(修改X为1)
a. L1 和 L2 中都没有发现变量 X,直到在主内存中找到
b 拷贝变量 X 到 L1 和 L2 中
c. 在 L1 中将 X 的值修改为1,并逐层写回到主内存中
在线程1访问之后,线程1的L1、共享L2、主内存中X的值被修改为1
线程2访问:(修改X为2)
a. L1 中没有发现变量 X
b. L2 中发现了变量X
c. 从L2中拷贝变量到L1中
d. 在L1中将X 的值修改为2,并逐层写回到主内存中
在线程2访问之后,线程2的L1、共享L2、主内存中的X都被修改为2
当线程1再次访问X:
发现自己 L1 中 X=1, L2 中 X=2 ,二者值不一样。
此刻,如果线程 1 再次将 x=1回写,就会覆盖线程2的 x=2 的结果,
同样的共享变量,线程拿到的结果却不一样(线程1眼中x=1;线程2眼中x=2),这就是共享变量内存不可见的问题。
即各个线程的工作内存不可见:
A线程先读取共享变量X, B线程修改了共享变量a后为X
推送给主内存并改写, 主内存不会推送给 A线程,
A和 B的变量会不同步
简单点来说就是不再参考 L1 和 L2 中共享变量的值,而是直接访问主内存
三、synchronized和volatile
1.volatile
线程在【读取】共享变量时,会先清空本地内存变量值,再从主内存获取最新值
因为会先清空本地内存变量值,再直接从主内存获取最新值,跳过了缓存,保证了只要使用volatile的变量,就会先从主内存同步
线程在【写入】共享变量时,不会把值缓存在寄存器或其他地方(L1或L2),而是会把值刷新回主内存
这样保证被volatile修饰的变量的值只要修改就会直接更新到主内存,不会使用缓存
这样就解决了共享变量内存不可见问题
2.synchronized
【进入】synchronized 块的内存语义是把在 synchronized 块内使用的变量从线程的工作内存中清除,从主内存中读取
【退出】synchronized 块的内存语义事把在 synchronized 块内对共享变量的修改刷新到主内存中
3.区别
这样看,二者都解决了内存可见性问题,但是二者有什么区别?
看例子:
类中有个变量 count,有两个线程对其进行自增操作10000次,
因为未加上述两种修饰符,如果多线程修改value,就会内存不可读导致最终结果小于20000
因为synchronized关键字不能修饰变量,因此将自增10000次封装成一个方法
如下图:
对于上图:
1.给count添加volatile关键字
这样虽然解决了内存可见性问题,但是count++不属于原子操作,这样最终结果会小于20000
因为volatile关键字不能保证原子性
2.给方法添加synchronized
synchronized 是独占锁/排他锁(就是有你没我的意思)
同时只能有一个线程调用 add10KCount 方法,其他调用线程会被阻塞。
所以三行 CPU 指令都是同一个线程执行完之后别的线程才能继续执行,这就是通常说说的原子性 (线程执行多条指令不被中断)
因此最终结果一定为20000.
四、什么时候使用volatile
如果写入变量值不依赖变量当前值,那么就可以用 volatile
synchronized 是排他的,线程排队就要有切换,要完成切换,还得记准线程上一次的操作,很累CPU大脑。
这就是通常说的上下文切换会带来很大开销
volatile 就不一样了,它是非阻塞的方式,所以在解决共享变量可见性问题的时候,volatile 就是 synchronized 的弱同步体现了
例如:
一个加油站,多个加油机。
这些加油机遵守加油站的定价标准。
不使用volatile:
1.加油站定价为10元/L
2.加油机1和2第一次加油时从主内存读取价格10元/L放入自己的本地内存L1和L2中
3.之后每次都从本地内存中读取;
4.此时,加油站改变定价为12元/L
5.因为加油机1和2本地内存都有定价10元/L,不会去主内存读取最新的价格,造成了一些问题。
分析:
上述例子可以发现,在加油机1和2中(也就是线程处理时,只读取价格的值,不修改价格的值或者说修改价格的值不依赖与价格原本的值)
此时可以使用volatile关键字修饰价格,解决可见行的问题。