一、计算机硬件相关概念
计算机硬件可以组成可以抽象为由总线、IO设备、主内存和处理器(CPU)组成。主内存用来存放数据,CPU用来执行具体指令。
1.1 单核模型
CPU执行指令会非常快,而从主内存中读取数据相对耗时,为了解决这一问题,一般会将需要运算的数据从主内存中复制一份都CPU中,又叫CPU的高速缓存,CPU进行运算时,就可以直接对高速缓存进行读写,待运算结束后,再将高速缓存的数据回写到主存中,这种模型可以简单的认为就是一个单核模型。
1.2 多核模型
计算机发展到现在,基本都是多核的处理器,也就是可以支持多线程并行计算,且CPU也存在一级缓存、二级缓存甚至三级缓存的区分,具体示意如下:
具体一级缓存、二级缓存、三级缓存的定义及区别,并非本文讨论的重点,自己了解也有些,这里不做详细说明,我们仅讨论这里可能会存在的缓存一致性问题。
举例来说,由于每个核里面都存在自己的缓存,数据会复制到各自的缓存中,那当并发的多个线程对同一个数据进行读写时,如果不加控制,必然会出现幻读或脏读等问题。
那CPU厂商是怎么来解决这个问题的呢?
有两种方式:
1.总线加锁。由于所有的数据读写都会经过总线,那一旦某个CPU持有了锁,那只要在总线上阻塞其他CPU,就能确保同一时间只有一个CPU操作数据,也就不会出现脏数据的问题。然而这种方式显而易见的效率低下,相当于将多核变为单核操作。
2.缓存一致性协议。主要有MSI、MESI、MOSI等,其主要思想为:如果某个CPU在对某个共享变量进行写的操作,会发出信号告诉其他CPU该变量的缓存目前是无效的,那么其他CPU在读取该变量时,会发现从自己的缓存中取不到有效的变量,就会从内存中重新读取新的值,也就是确保了可见性。
二、JAVA内存模型
类比计算机硬件,JAVA内存模型,也存在主内存和工作内存,简单示例如下:
可以看到java线程其实是直接操作的工作内存,我们按读和写两条线来看下这里面涉及的操作:
读:
read(读取):从主内存读出来
load(载入):将read到的值载入工作内存
use(使用) :将工作内存中的变量取出给java线程使用
写:
assign(赋值):由java线程将变量赋值给工作内存
store(存储) : 工作内存将变量传递给主内存
write(写入) : 将已经store的值写入主内存
lock & unlock:
作用于主内存将某一变量只能被一个线程锁定/释放。
可以看到,java线程读的过程其实是read>>load>>use的过程,而写的过程则是assign >> store >> write的过程,显而易见,如果load之后发生了write操作,则接下来java线程use到的值,将会是之前的旧值。
三、volatile
实际上volatile做了两件事
1.通过在上面这些指令前后增加屏障,确保了单次读的过程一定是read>>load>>use,单次写的过程一定是assign >> store >> write。
2.同一个线程内,如果按照程序顺序,先后发生读、写,则被volatile修饰的变量不会被指令重排序,保证代码的执行顺序和程序的顺序相同。
做到了上述两点,也就是我们说的volatile的变量保证对所有线程的可见性,以及禁止了指令重排