从动图我们看到,A线程内部判断 "initFlag" 变量,如果变量false则一直进行循环,而代码中的B线程内部调用refresh()方法将变量 "initFlag" 修改为true,而此时A线程内部的循环感应到 "initFlag" 变量为true了应该退出来才对,而为什么演示图中A线程内部的循环并没有退出来?
带着这个疑惑,将代码稍微改动一下,这次在代码中定义一个全局变量为count为int类型,并在A线程循环中,将变量count自增操作,再来看看它的效果如何
上图,我们往循环内部加一个count自增操作,貌似并没有解决掉A线程循环的退出对吗?不慌,这次我们往count变量上加一个volatile关键字,接着来看看效果
欸,上图中,我往count变量加了一个volatile关键字,A线程内部的循环居然退出去,那么意味着A线程它检测到了全局变量 "initFlag" 将值改变为true了,那我们再接着测试一下,我们将count++操作去掉,往initFlag变量上加volatile关键字,在继续看看效果如何?
嗯,好家伙,似乎经过这两轮测试,其实可以大致猜出加了volatile关键字的原因,该篇文章不是讲volatile的重点,我来讲讲为什么发生这种情况。
这张图,我前面已经放上去了,现在再次粘过来,是为了更好的说明上述程序的问题所在,我上面有解释过主内存是专门存储成员变量的,该成员变量的值是允许被多个线程进行共享,那么上述程序中 "initFlag" 变量作为成员变量,是可以被A线程和B线程进行读取和操作的,那么此时A和B线程都会要进行读取共享变量,它们各自会从主内存中将变量进行拷贝到各自线程内部的工作内存中,接着B线程内部调用了refresh()方法,将initFlag的值改为true,在jmm模型中,B线程它并不会直接将值改回到主内存中,而是先将自己内部的工作内存的 "initFlag" 值改为true,然后再写回主内存中,虽说此时主内存的值已经发生了改变,但是A线程内部的循环的判断,还是在使用它自己内部工作内存中的 "initFlag" 的值,它并没有及时的知道共享变量的值已经发生了改变,所以这就导致了A线程长时间无法走出循环的原因。
而我后面又在initFlag变量上加了volatile关键字,为什么能够立马感知到呢?
JMM内存模型定义
JMM内存模型主要通过三个特征组建成,1.原子性 2.可见性 3.有序性.这三个可谓是java并发的基础
原子性:
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
可见性:
可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java是利用volatile关键字来提供可见性的。当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点。(如果其他线程使用到了该变量,修改后会立刻刷新到主内存,并且主动推送到其他线程的工作内存中更新该变量值)看到此处,是不是就知道为什么加了volatile关键字,其他的线程能够立马感知到变量发生了变化。
有序性:
在并发情况下,能够让线程按代码从上往下按顺序进行执行,可以使用synchronized或者volatile保证多线程之间操作的有序性(这种是在没有指令重排的情况下)
通过对前面的硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行,但Java内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存(线程私有数据区域)和主内存(堆内存)之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。
结论:在JVM的内存模型中,每个线程有自己的工作内存,实际上JAVA线程借助了底层操作系统线程实现,一个JVM线程对应一个操作系统线程,线程的工作内存其实是cpu寄存器和高速缓存的抽象。
在文章上面,我对JMM模型的代码案例以及图都做了一个比较清楚的解释,但是主内存中的共享变量的值是如何copy到线程内部的工作内存中的呢?这里就涉及到数据同步八大原子操作,且看下图。
(1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定(3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用 (4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中 (5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎 (6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量 (7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作(8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
结合上面的代码例子,通过八大原子操作,实现的流程
1)不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中2)一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。3)一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。4)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。5)如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。6)对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
最后看到这,我前面似乎还漏了一个问题没有讲到,我稍微回顾一下案例场景,上面代码案例中,我定义了一个 initFlag变量,通过B线程将变量的值进行改变,发现A线程循环内部无法跳出循环的问题,后来又额外的定义了一个count变量,用于在A线程循环内部做自增的操作,然后让它进行跳出来,发现该方法并不可行,然后又继续往count值上加了一个volatile关键字,它就能够立马被A线程感知到,看到这可能还感受不到问题的存在,那么再仔细想想,结合前面的JMM内存模型的图,我在initFlag变量上加了volatile关键字,它能够被立马感知到,这是非常符合逻辑的,但是问题出现在于为什么我将关键字加在了count变量上,initFlag变量也能够被感知到呢?
这里我想回答的是,在cpu底层的缓存行中,它的每个缓存行大小为64个字节,而我们的initFlag变量它只占用了一个字节,且count变量它占用了4个字节,它们在缓存行中总共5个字节,当缓存行中的某一个变量的值发生了修改,volatile关键字会强行通知线程去拉取最新变量的值。所以这就是为什么我在count变量上加了关键字,其他线程能够及时的感知到initFlag的值发生了改变的原因。
最后我还想说明的一点是,无论我们是否加了volatile关键字,线程迟早会知道变量发生了改变,只不过区别在于关键字能够及时的通知线程变量发生了改变。
我是黎明大大,我知道我没有惊世的才华,也没有超于凡人的能力,但毕竟我还有一个不屈服,敢于选择向命运冲锋的灵魂,和一个就是伤痕累累也要义无反顾走下去的心。
如果您觉得本文对您有帮助,还请关注点赞一波,后期将不间断更新更多技术文章
发现“在看”和“赞”了吗,因为你的点赞,让我元气满满哦