首先拿一个例子,该例子简单明了的展示出一个问题:
如果flag 不加Volatile关键字时,每当另外一个线程对flag进行操作的时候,其他线程是感知不到的;当在flag 前加上Volatile关键字后,每当其他线程修改了flag值得时候,其他线程也都可以感知到flag进行了变更,会从读取最新的值。
这就是Volatile关键字的其中一个作用:保证可见性。
volatile只是保证了可见性,并不算是轻量级锁,其作用是: 1.保证可见性 2.禁止指令重排 3.不保证原子性
目录
四、java内存模型简易图(不加Volatile关键字类似上图)
一、VolatileDemo
public class MyVolatileDemo { static int flag = 0; //加上了 volatile关键字 每次修改flag值 第一个线程立即感知到flag发生改变 // static volatile int flag = 0; public static void main(String[] args) { new Thread(() -> { int localFlag = flag; while (true) { //如果 flag 不加volatile关键字 这个线程一直感知不到flag发生改变,一直读取的是旧值,所以一直不会执行 if (localFlag != flag) { System.out.println("我感知到flag值发生了变化 " + flag); localFlag = flag; } } }).start(); new Thread(() -> { int localFlag = flag; while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("标志位被修改为:" + ++localFlag + "..."); //flag值 每秒钟都在被修改 flag = localFlag; } }).start(); } }
不加Volatile关键字时,运行结果:
加上关键字,运行结果:
static volatile int flag = 0
二、CPU多级缓存与可见性
提到可见性的问题,我们可以简单分析一下为什么会造成其他线程不能实时感知到flag的更改。这根计算机cpu多级缓存有关;cpu内部有多级缓存,在操作数据是在自己内部的高速缓存中进行,这样大大提高了cpu处理效率,到一定时候会刷入主存。如果多个cpu直接读取主存中的数据,会造成效率和性能的低下。画个简单的图解:
三、CPU缓存模型与Volatile关键字
不加Volatile关键字——多线程并发问题
四、java内存模型简易图(不加Volatile关键字类似上图)
比如两个线程同时读取flag=0,其中一个线程1读取之后一直进行操作,并且会刷回主内存。这时线程2每次循环读取自己工作内存中初始加载的值,并未感知其他线程修改了flag原来的值,所以在并发执行时造成数据的不一致 。
flag 加上volatile关键字,当线程1对flag进行操作后,flag=1。assign步骤将flag=1写入到共工作内存,并且也会被强制刷入到主内存中;此时主存中flag=0 ——> flag = 1;此时线程2中,立马感知到flag内修改,自己本地工作内存中flag=0失效,并重新读取主内存的flag值,即更新后的flag=1。
五、为了达到数据访问的一致的解决方案
总线加锁:
总线锁就是将cpu和内存之间的通信锁住,使得在锁定期间,其他cpu处理器不能操作其他内存中数据,故总线锁开销比较大。
总线锁的实现是采用cpu提供的LOCK 信号,当一个cpu在总线上输出此信号时,其他cpu的请求将被阻塞,那么该cpu则独占共享内存
缓存一致性协议:为了达到数据访问的一致,需要各个处理器在访问 高速缓存 时遵循一些协议,在读写时根据协议来操作,常见的协议有MSI、MESI、MOSI。最常见的是MESI协议。
在MESI协议中,每个高速缓存的 缓存控制器 不仅知道自己的读写操作,而且也监听其他告诉缓存的读写操作。共有四种状态,分别是:
- M(Modify)表示共享数据只缓存在当前CPU缓存中,并且是被修改的状态。此时表示当前CPU缓存数据与主内存中不一致,其他CPU缓存中如果缓存了当前数据应是无效状态,因为该数据已被修改且并没更新到主内存
- E(Exclusive)表示缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改
- S(Shared)表示数据可能被多个CPU缓存,并且各个缓存中的数据和主内存中的数据一致
- I(Invalid)表示当前缓存已经失效
当Flag变量 加上Volatile关键字后,作用就跟缓存一致性协议类似,但一个线程修改数据之后,会强制刷入主存,其他线程也在监听自己内存的flag是否被修改,如果被修改,则当前flag过期,立即去主存加载最新flag值。以达到数据保持一致。
六、Volatile关键字为什么不保证原子性
我们都知道Volatile关键字的三个特点,这里不做介绍。但Volatile关键字并不能保证原子性,知其然也要知其所以然,今天增加一个简易图来简单阐述Volatile关键字不能保证原子性的原因。
上面图中有五个步骤,线程2在自己工作内存操作flag++时,只是基于之前内存中的值,即使工作内存感知到flag被线程1修改之后,但线程2操作仍然是之前读取的flag=0,也不能影响线程在flag=0的基础上进行++操作,最终线程2assign flag=1重新赋值给工作内存中的flag=1。最后写入主存仍然是与线程1操作结果一样,仍然是flag=1。