volatile原理及深入浅出JMM

volatile原理

volatile关键字是用来保证有序性和可见性的。

首先我们先来解释一下有序性。
这跟Java内存模型有关。比如我们所写的代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排序,处理器也会做重排序的,这样的重排序是为了减少流水线的阻塞的,引起流水阻塞。
当然,不能只为了提高CPU的执行效率,相应的需要有一定的顺序和规则来保证,比如数据依赖性,对于a=1,b=a,是不会改变执行顺的,不然程序员自己写的代码都不知带对不对了。
所以有JMM提供了happens-before规则(在。。发生之前):注意:两个操作之间具有happens-before关系,并不意味着前一个操作必须在后一个操作之前执行!happens-before仅仅要求前一个操作的结果对后一个操作可见,且前一个操作只要在后一个操作之前就可以。
下面是happens-before的集中规则:
1.程序顺序规则:一个线程中的每个操作 发生在该线程中的任意后续操作之前。(就是让线程的执行顺序按照代码顺序执行)
2.监视器锁规则:对一个锁的解锁 发生在随后对这个锁加锁之前
3.volatile变量规则:对于一个volatile域的写发生在任意后续对这个volatile域的读之前
4.传递性:如果A happens-before B B happens-before C,那么A happens-before C
5.start规则:如果线程A执行线程B的start方法,那么线程A的ThreadB.start()happens-before于线程B的任意操作
6.join规则:如果线程A执行线程B的join方法,那么线程B的任意操作happens-before于线程A从TreadB.join()方法成功返回。

其中有条就是volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
volatile的有序性是通过插入内存屏障来实现的,这里就不具体介绍内存屏障了。

首先看看volatile读写哪种情况下,不能重排序。
1.当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序;
2.当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序;
3.当第一个操作是volatile写,第二个操作是volatile读,不能重排序;

但是对于发现一个最优的插入内存屏障是不可能的,JMM就在每个volatile读写前后都插入内存屏障来实现有序性。

接下来我们解释一下可见性
首先简单介绍一下Java内存模型,分为主内存(就是所有线程共享的堆和方法区资源),工作内存(就是为每个线程创建属于自己的空间,比如虚拟机栈,本地方法栈)。
下面介绍一下可见性问题如何产生,可以参考下图
比如线程A从主内存把变量从主内存读到了自己的工作内存中,做了加1的操作,但是此时没有将i的最新值刷新会主内存中,线程B此时读到的还是i的旧值。
可见性问题
加了volatile关键字的代码生成的汇编代码发现,会多出一个lock前缀指令。Lock指令对Intel平台的CPU,早期是锁总线,但是锁总线,只有该处理器独占共享内存,其他处理器的请求都会被阻塞住。这样代价太高了,后面提出通过缓存锁定来保证原子性。就是在#lock期间,它会锁定该内存区域的缓存并写会内存,期间通过缓存一致性保证修改的原子性, 就是我们只保证对某个内存地址的操作是原子性的就可以。
下面简单说一下缓存一致性:
缓存一致性协议:在多处理器下,为了保证各个处理器的缓存是一致的,每个处理器通过嗅探在总线上的传播的数据来检查自己缓存中的数据是否过期,当处理器发现自己缓存行对应的内存地址发生变化,就会是该缓存行中的数据无效,当处理器再次对这个数据进行修改时,会重新从系统内存中把该数据读到处理器缓存中,
缓存一致性会阻止同时修改两个以上的处理器缓存的内部数据。

有volatile修饰的共享变量在进行写操作时,会多出lock的汇编代码,而lock前缀的指令在多核处理器下会进行两个操作
1.将当前缓存行中的数据写回到系统内存中。
	LOCK#信号会锁定这块内存区域的缓存,并写回内存,并使用缓存一致性,保证它的原子性。
2.这个写回内存的操作会使其他cup里缓存了该内存地址的缓存行无效。

伪共享问题及volatile增强
上面的第2步会把其他cpu中缓存行中的数据无效,但如果该缓存行中不是存放一个元素,而是多个,比如ArrayBlockingQueue,当cpu从内存中加载takeIndex出队索引时,同时也会把入队索引putIndex和队列中元素总数count加入该缓存行中,若线程1入队操作,线程2出队操作,两个线程都会读取内存到自己的cpu中的缓存行中。线程1入队修改putIndex,会使线程2中的缓存行失效,不能很好利用缓存,叫伪共享。不过这里只是提到,ArrayBlockingQueue的入队和出队操作用锁来保证互斥,不会同时发生。
cpu缓存图
其实仔细想想,你会发现volatile修饰符修饰变量会发生上面的情况,所以JDK7之后就是使用缓存行填充的方式进行优化,让一个变量独享一个缓存行。

发布了34 篇原创文章 · 获赞 0 · 访问量 1089

猜你喜欢

转载自blog.csdn.net/qq_42634696/article/details/104648913