说到JMM,大家一定很陌生。我们所知道的肯定是jvm虚拟机,而我们今天说的JMM与JVM虚拟机无关,不要把JMM的任何东西和JVM联系起来,把JMM当成一个全新的东西来理解和认识。我们先来看计算机的理论模型,也就是冯·诺依曼计算机模型。首先,让我们拍张照片。其实我们更关注计算机内部CPU的计算和内存之间的关系。让我们仔细看看它是如何计算的。我们来看看这个东西的处理流程。当我们的数据和方法加载的内存区域需要处理时,内存将数据和方法传递给CPU的L3->L2->L1,然后进入CPU进行计算。然后从L1->L2->L3->回到主存,但是我们的技术发展很快。现在好像没有单核CPU了。什么8核16核CPU随处可见。我们这里唯一的CPU只是CPU的一个核心来计算这些东西。假设我们的方法是f(x)=x+1,我们的输入参数是1,预期结果是2,1+1=2,我的计算是正确的。如果我们的两个核心同时执行该方法怎么办?为什么我们的CPU2响应有点慢?如果当我们的内存给CPU2发送参数的时候,CPU1可能已经计算完成返回了。此时CPU2得到的参数为2,此时的计算为2+1。结果不是我们所期望的。这其实是数据不同步造成的。我们应该尽量同步数据。 我们在中间加了一层缓存一致性协议。那就是我们的MESI。在多处理器系统中,每个处理器都有自己的高速缓存,并且它们共享相同的主内存。我简单说一下我们的MESI是什么,是怎么做的。缓存一致性。英文不好,大家解释一下MESI是什么的首字母缩写我也没有看错(不知道是不是缩写,但是我知道是怎么回事)。我们仍然有从内存到CPU的这条线。这时,我们多了一个MESI。当一起读取变量X时,CPU1和CPU2共享一个X变量,但它们分别存储在CPU1和2中,也就是我们的X(S)状态。然后CPU1和2准备一起计算。那么1和2肯定会有一个厉害的。比如1赢了,此时CPU1中的X(S)由共享态变为独占态的X(E),并告诉CPU2将X(S)变为X(I)的状态,从共享状态变为无效。然后CPU1就可以计算了。计算完成后,X(S)变为X(M)状态,独占状态变为修改状态。M:Modified(修改)缓存行只缓存在CPU的缓存中,被修改(dirty),即与主存中的数据不一致。cacheline中的内存需要在以后存储。在某个时间点(在允许其他CPU读取主存中的相应内存之前)回写到主存。写回主存后,缓存行的状态将变为独占。E:独占(Exclusive)缓存行只缓存在CPU的缓存中,没有被修改(clean),与主存中的数据一致。当其他CPU读取内存时,此状态可以随时共享。同样,当CPU修改缓存行的内容时,状态可以变为Modified状态。S:Shared(共享)该状态表示缓存行可能被多个CPU缓存,每个缓存中的数据与主存数据一致(clean)。当一个CPU修改缓存行时,其他的CPU会将一个缓存行无效化(becomeinvalid)。I:无效(Invalid)说到这里,这就是我们JMM内存模型的工作机制。所以JMM是虚拟的,与JVM无关。切记不要混淆。这里还有三个重要的知识点。JVM内存模型(JMM)的三大特点:原子性:表示一个操作是不可中断的。即使多个线程一起执行,一旦一个操作开始,也不会受到其他线程的干扰。比如对于一个静态全局变量inti,两个线程同时给它赋值,线程A赋值1,线程B赋值-1。所以无论两个线程如何工作,以什么速度工作,i的值要么是1,要么是-1,线程A和线程B之间没有干扰。这是原子性的一个特点,不间断的可见性:当一个线程修改一个共享变量的值,其他线程是否可以立即知道修改。显然,对于串行程序,不存在可见性问题。因为你在任何一个操作步骤修改了一个变量,那么在后面的步骤中,这个变量的值一定是修改后的新值。但是这个问题在并行程序中就不一定了。如果一个线程修改了某个全局变量,其他线程可能不会立即知道变化。有序性:对于一个线程的执行代码,我们总是习惯认为代码的执行是从头到尾依次执行的。这种理解不能说是完全错误的,因为对于一个线程来说,确实是这样的。但是,在并发的情况下,程序的执行可能会出现乱序。直观的感觉就是写在前面的代码会在后面执行。顺序问题的原因是程序执行时,可能会重新排列指令,重新排列后的指令顺序可能与原来的指令不一致(后面会讲到指令重排)。我们来看看volatile关键字,先看一段代码。不去看代码,总觉得自己没有练好。privatestaticintcounter=0;publicstaticvoidmain(String[]args){for(inti=0;i<10;i++){Threadthread=newThread(()->{for(intj=0;j<1000;j++){counter++;//不是原子操作,第一次循环的结果没有刷入主存,本次循环无效}});thread.start();}try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println(counter);}为了按照JMM的思维过程来解释这段代码,我们首先创建了10个线程。我们在这里称之为T1、T2、T3...T100。然后分别取计数器编号,然后叠加1,循环1000-计数器次数。当T1拿到计数器后,就开始计算。如果,当我们计算到第50次时,线程T2也开始获取counter的个数。这时候得到的counter个数是50,那么T2会循环950次,最后我们计算出来的counter是9950,也就是说内部没有内存一致性协议。所以我们的输出必须是一个<=10000的数字。让我们尝试更改代码并使用我们的volatile关键字。privatestaticvolatileintcounter=0;publicstaticvoidmain(String[]args){for(inti=0;i<10;i++){Threadthread=newThread(()->{for(intj=0;j<1000;j++){counter++;//不是原子操作,第一次循环的结果没有刷入主存,本次循环无效}});thread.start();}try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println(counter);}这时候我们加入了volatile关键字,我们会发现经过多次运行,每次的结果都是10000,也就是说每次我们期望的都是As结果,volatile可以保证线程可见性并提供一定的顺序,但不能保证原子性。在JVM的底部,volatile是使用“内存屏障”实现的。也就是说,当我们加入volatile关键字后,java代码在运行过程中会强制执行一层内存一致性屏障。如果这样做了,我们的计算就不会直接相互影响,就会得到我们预期的结果。1.可见性实现:如前所述,线程本身并不直接与主存进行数据交互,而是通过线程的工作内存来完成相应的操作。这也是线程间数据不可见的本质原因。因此,要实现volatile变量的可见性,可以直接从这方面入手。volatile变量和普通变量的写操作主要有两点不同: (1)当一个volatile变量被修改时,会强制在主存中刷新修改后的值。 (2)修改volatile变量后,其他线程工作内存中对应变量的值将失效。因此,再次读取变量值时,需要重新读取主存中的值。相当于上述过程从S->E,另一个线程从S->I。 通过这两个操作,可以解决volatile变量的可见性问题。2.内存屏障用于实现可变可见性和happen-befor语义。JVM的底层是通过一种叫做“内存屏障”的东西来完成的。内存屏障,也称为内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。以下是完成上述规则所需的内存屏障:Requiredbarriers2ndoperation1stoperationNormalLoadNormalStoreVolatileLoadVolatileStoreNormalLoadStoreNormalStoreStoreStoreVolatileLoadLoadLoadLoadStoreLoadLoadLoadStoreVolatileStoreStoreLoadStoreStore(1)LoadLoad屏障执行顺序:Load1—>Loadload—>Load2确保Load1加载的数据在Load2和后续的Load指令加载数据之前可以访问到。(2)StoreStore屏障执行顺序:Store1—>StoreStore—>Store2保证在Store2及后续Store指令执行前,Store1操作的数据对其他处理器可见。(3)LoadStore屏障执行顺序:Load1—>LoadStore—>Store2,保证Load1加载的数据在Store2及后续Store指令执行前能被访问到。(4)StoreLoadbarrier执行顺序:Store1—>StoreLoad—>Load2保证在读取Load2和后续Load指令之前,Store1的数据对其他处理器可见。总的来说,volatile的理解还是比较难的。如果您不是很了解,请不要担心。完全理解它需要一个过程。在后续的文章中你也会多次看到volatile的使用场景。到这里我们对volatile的基础知识和原来的有了一个基本的了解。一般来说,volatile是并发编程中的一种优化,在某些场景下可以替代Synchronized。但是volatile并不能完全替代Synchronized,volatile只能应用于一些特殊的场景。一般情况下,要保证并发环境下的线程安全,必须同时满足以下两个条件: (1)对变量的写操作不依赖于当前值。 (2)该变量不包含在与其他变量的不变量中。参考地址:https://www.cnblogs.com/paddix/p/5428507.htmlJMM-同步的八个操作简介(一)锁(lock):作用于主存的变量,将一个变量标记为一个线程独占状态(2)解锁(unlock):作用于主存的变量,释放处于锁定状态的变量,释放后的变量可以被其他线程锁定(3)读(read):变量actionsonthemainmemory,将一个变量值从mainmemory传递到线程的workingmemory,以便后续的loadaction使用(4)load(loading):作用于workingmemory的变量,传递变量从主存读操作得到的值放入工作内存的变量副本(5)使用(use):作用于工作内存的变量,将工作内存中的一个变量值传递给执行引擎(6)赋值(assign):作用于工作记忆的变量,它给接收到的值赋值从执行引擎到工作内存中的一个变量(7)存储(storage):作用于工作内存中的一个变量,将工作内存中的一个变量的值传送到主内存中,以供后续的写操作(8)写(write):作用于工作内存的变量,将存储操作从工作内存中的变量值传递到主内存中的变量。流程图大致是这样的:
