文章目录
- 前言
- 一、Java的内存模型
- 二、保证可见性的方式
- volatile
- synchronized
- lock
- final
- 三、volatile的底层实现
- 总结
前言
在讨论这个问题之前,我们可以先瞅瞅Java的内存模型JMM,JMM可不要和JVM混为一谈。我们说的是内存模型JMM(Java Memory Model)
。
一、Java的内存模型
稍微解释一下CPU的缓存,这里CPU的缓存有三级,L1,L2和L3。
- L1是访问速度最快的,是线程独享。
- L2次之,属于内核独享。
- L3是最慢的,是多核共享。
当CPU执行指令需要数据的时候,会先在L1,L2,L3中依次寻找,若是找不到,则会去JVM中寻找,而JMM则在CPU和主内存之间来保证我们需要的可见性和有序性。
这个JMM就是Java内存模型的核心,可见性有序性都是在这里实现。
而主内存就是JVM,就是我们的堆内存。
二、保证可见性的方式
可见性是指,当多个线程操作同一个数据的时候,保证一个线程的修改对其他线程是可见的,也就是说不管多个线程如何操作,如何并发,他们在同一时间取到的值是相同的。
为了保证可见性,我们一般有以下几种方案:、
volatile
可以用volatile来修饰基本数据类型,可以保证每次CPU操作数据的时候,都直接操作的是主内存的值。
synchronized
对于synchronized来说,是谁拿到锁,谁执行操作,对于拿到锁的线程来说,前边线程的操作是可见的。
lock
lock是基于CAS和volatile的修改操作,可以保证操作数据时前边操作的可见性。
final
final修饰的是常量啊,没法写,只读,当然是全局可见的
这里有一个小点:
我们清楚,volatile修饰基本数据类型的时候是可以保证可见性的,但是若是修饰的是引用数据类型呢?
一般没人这么搞,甚至平时工作中volatile都很少使用,一不留神系统的性能会降低几个维度。但是面试中常被问到,我们可以这样回答:若volatile修饰的是引用数据类型,则只能保证引用数据类型的地址是可见的,里边的值不可见。就这。
三、volatile的底层实现
稍微看看volatile的底层实现吧,其实
volatile的底层是汇编的lock指令,这个指令会强行要求将值写入主内存,并且忽略Store
Buffer这种缓存,从而达到可见性的目的,同时利用MESI协议,让其他缓存行失效。
我们晓得将java文件编译为class文件的时候,会基于JIT做优化,调整指令的顺序,从而提升执行效率,这个过程叫指令重排。
在CPU层面也会调整指令的顺序来提升性能。而这个指令重排会导致一些问题,我们看看volatile是如何解决这个问题的。
被volatile修饰的属性,在编译时会在先后增加内存屏障。这里的提到的内存屏障一般有四种
- SS: StoreStore屏障前的读写操作必须全部完成,才会继续屏障之后的操作
- SL: StoreLoad屏障前的写操作必须全部完成,才会继续屏障之后的读操作
- LL: LoadLoad屏障前的读操作必须全部完成,才能继续屏障之后的读操作
- LS: LoadStore屏障前的读操作必须全部完成,才能继续屏障之后的写操作
这里volatile的原理就如下
- 可以看到对于volatile的写操作
之前添加了StoreStore内存屏障,必须完成之前的读写操作才能继续volatile的写操作。
而后添加了StoreLoad屏障,要求其前边的volatile写操作完成,才能继续之后的读操作。
这样就保证了在对volatile修饰的值执行写操作的时候,之前的读写操作已经全部完成,而其后的读操作在等待写操作完成再去读值。
- 而对于volatile的读操作,在其后添加了一个LoadLoad屏障和LoadStore屏障,这里这个LoadLoad屏障不是很理解,查阅了诸多资料也没有眉目,若是有小伙伴知晓的,烦请不吝赐教。
而LoadStore屏障则保证了volatile的读操作全部完成之后,再继续之后的写操作。
这样volatile的读操作完成之后,才会执行其他的写操作。保证了读到的值是确定的不变的。
总结
Java的线程间共享值的处理方式就大致讲解完毕了,若有不懂的地方,尽情留言,我们一起讨论,学习,感谢。