前言说Volatile关键字之前先说一下cpu多核并发缓存架构,然后是JMM,也就是java内存模型,最后是volatile关键字。JMM(JavaMemoryModel)多核并发缓存架构的引入为了解决CPU与主存速度不匹配的问题,计算机在中间设计了多级缓存(通常放在内存内部)CPU,这里为了更好看)中),缓存读取速度非常快,CPU与缓存交互,程序结束后,缓存中的数据会同步到主存中,然后写入回硬盘。Java线程的内存模型与CPU缓存模型类似,都是基于CPU缓存模型。Java线程的内存模型是标准化的,屏蔽了不同底层计算机之间的差异。如下图所示:和CPU一样,线程A为了解决主存速度不匹配的问题,会将这个共享变量复制到线程的工作内存中。读取共享变量数据的线程与工作内存中的变量副本进行交互。这里的工作内存类似于缓存。JMM数据原子操作JMM有8个原子操作,按使用过程排序,分别如下:这里介绍java的数据原子操作,是为了更好的为后面的问题做铺垫。CPU缓存不一致问题对于多核CPU,当共享变量同时加载到缓存中,同时在多个核上进行操作时,核心A修改变量a时,核心B并不知道a已经被修改.继续推进核心B的线程工作,这样程序就会出现问题,所以出现了缓存不一致的问题。为了解决CPU缓存不一致的问题,工程师们主要采用了两种方法。早期主要采用总线锁定方式。总线锁定:即CPU从主存中读取数据到缓存中,并在总线上锁定数据,使得其他CPU核心无法读取或写入数据,直到CPU使用完数据并释放锁。cpu核心可以读取数据。这种方式可以通过java内存模型和java数据原子操作来体现,如下图所示:在主存中,只有释放锁才能读取该变量的数据,才能读取该变量的值并在其他CPU中进行计算。锁操作会在读之前进行,标记为线程独占状态。当写回主存时,会进行解锁操作。解锁后,其他线程可以锁定该变量。为了解决可见性和一致性的问题,早期的CPU把一个并行执行的程序变成了串行执行。显然,这种解决方案是不可行的。后来工程师使用MESI缓存一致性协议解决了这个问题。MESI缓存一致性协议:多个CPU从主存中读取相同的数据到各自的缓存中。当其中一个CPU修改缓存中的数据被删除后,数据会立即同步回主存,其他CPU可以通过总线嗅探机制感知到数据变化,并使自己缓存中的数据失效。如下图所示:CPU和内存通过总线相连。每个线程从主存中读取数据,实现并行。当线程2修改initFlag变量并执行store操作时,会将工作内存中修改后的数据initFlag=true变量的值写回主内存,最后执行write替换主内存中的值。一旦执行了store原子操作,数据就会通过总线写回主存。MESI缓存一致性协议有一个CPU总线嗅探机制(由硬件实现):当其中一个线程(这里是线程2)修改了变量的值从工作内存写回到主内存时,只要数据经过总线,其他CPU(这里是线程1)会监听总线,不断监听总线上感兴趣的变量的数据流向,发现有其他CPU(这里是线程1)感兴趣的时候变量,MESI缓存一致性协议将通过总线嗅探机制使另一个CPU(这里是线程1)的工作内存中的相同变量的值无效。然后线程1再次执行循环操作时,发现initFlag无效,再次从主存中读取initFlag。此时主存中的initFlag已经被修改(true),线程1可以拿到最新的值。向上。通过MESI缓存一致性协议和总线嗅探机制,程序可以实现缓存一致性。Java代码演示不可见性说完CPU缓存不一致的解决方案,接下来我们通过java代码演示多线程下缓存不一致的问题,也就是所谓的不可见性。公共类JMM{privatestaticbooleaninitFlag=false;publicstaticvoidmain(String[]args)throwsInterruptedException{newThread(()->{while(!initFlag){}System.out.println("hello...");}).start();TimeUnit.SECONDS.sleep(2);newThread(()->{System.out.println("...");initFlag=true;System.out.println("修改成功...");}).start();}}``````查看输出结果:代码运行后,只输出线程2的信息。主要原因是看不到双核CPU。可以看出在多线程的情况下,java代码的共享变量initFlag也是看不见的,那么,java是怎么解决缓存不一致的问题的呢?引入了Volatile关键字#Volatile的作用我们使用volatile修改变量initFlag来查看代码的运行状态。公共类JMM{privatestaticvolatilebooleaninitFlag=false;publicstaticvoidmain(String[]args)throwsInterruptedException{newThread(()->{while(!initFlag){}System.out.println("hello...");}).start();TimeUnit.SECONDS.sleep(2);newThread(()->{System.out.println("...");initFlag=true;System.out.println("修改成功...");}).start();}}线程2对initFlag的修改,线程1中的initFlag是可以感知的,即java的关键字volatile可以解决缓存一致性问题。#volatile如何解决缓存一致性问题关于什么?volatilecachevisibility实现原理:底层实现主要是通过锁前缀指令的组装,将锁定这块内存区域的缓存(cachelinelocking),并写回主存。IA-32ArchitectureSoftwareDeveloper'sManual对lock指令的解释:1)它会立即将当前处理器缓存行的数据写回系统内存2)这种写回内存的操作会导致其他CPU缓存的数据thememoryaddressInvalid(MESI)#看不懂上面说的是什么?没关系,一共记住3点:1)立即将当前处理器缓存行的数据写回系统内存2)这个写回内存的操作会导致缓存在其他内存地址的数据CPUstobeinvalid(MESI)3)在Lockbeforestore,andunlockafterwrite通过将上面的Java程序转成汇编代码来查看(b站老师之前转过,具体我没转,相当麻烦,他的截图留在这里)#Java中的缓存一致性问题可以从中看出Java内存模型,指的是CPU缓存模型,所以在多核多线程的情况下存在缓存一致性问题。从第5点可以看出,java使用volatile关键字来处理缓存一致性问题。那么,java是如何通过实现volatile来解决缓存不一致问题的呢?Java参考了CPU的解决思路,同时结合了总线锁和MESI缓存一致性协议,结合了MESI缓存一致性协议=volatile的锁实现,同样解决了缓存一致性问题。具体如下:线程1和线程2可以同时从主存中读取共享变量initFlag到各自的工作内存中,然后调用各种执行引擎对该变量进行处理,在添加volatile指令后shared变量,线程2中执行initFlag=true,会加上lock前缀的汇编指令,使得CPU底层立即将修改后的工作内存拷贝变量的值写回系统内存。而且,当这个数据经过总线时,利用CPU总线上的MESI缓存一致性协议和CPU总线嗅探机制,使其他CPU缓存中的同一个拷贝变量失效,同时锁住这块内存区域的缓存(即即将存储到缓存区的内存区域被锁定时),当存储返回到主存时,会先进行一次加锁操作,然后进行一次解锁(写后)操作回写后执行。这样就可以解决缓存一致性问题。#和总线锁的区别volatile的底层实现是:锁结合MESI缓存一致性协议的实现,这个实现和总线锁有什么区别?volatile大大降低了这个锁的密度,性能非常高。开始读的时候,每个CPU都可以读,但是写回主存的时候,其他CPU就不能操作了。如果volatile不加锁操作和解锁操作,只用缓存一致性协议和总线嗅探机制。有什么问题吗??没有加锁,数据刚刚同步到总线上(也就是刚回写到主存),而这个数据还没有写到主存中的变量中(也就是变量initFlag还没有写完)已改为true),而其他CPU则通过MESI缓存一致性协议中的总线嗅探机制检测到initFlag值的变化,并立即使其他线程中工作内存的值无效。而另一个CPU(线程1)还在执行while操作,发现initFlag无效,就立即从主存中读取initFlag。这个线程2还没有立即将initFlag修改后的值写入主存,所以此时其他CPU(线程1)还在读取原来的旧数据。所以lock前缀指令必须在store之前加锁,真正写入主存后释放锁,只是为了防止一些数据的误读(时差的问题),这个锁的密度非常大小,只要给主存赋值,内存操作就快很多,内存级别的并发至少是每秒几十万、上百万次操作。就做变量地址的赋值操作,这么短的时间加锁,速度非常快!!!#volatile不保证原子性说到这里大家应该清楚并发编程的三大特点:可见性、原子性和有序性。volatile可以保证可见性和有序性,但是不能保证原子性。原子性可以借助synchronized锁机制或者concurrent包下的atomic类,这个原子性会在下一篇博文中总结。该代码演示了volatile不保证原子性。公共类VolatileAtomicTest{publicstaticvolatileintnum=0;publicstaticvoidincrease(){num++;//num=num+1}publicstaticvoidmain(String[]args)throwsInterruptedException{Thread[]threads=newThread[10];for(inti=0;i
