文章目录
概要
在网上的很多文章当中,都说到了volatile不可以保证原子性,对于这个关键字的定义解释的很清楚。
volatile 是 Java 中的一个关键字,用于修饰变量,它的主要作用是保证变量的可见性和禁止指令重排序。 但是对于为什么这个关键字不能保证原子性这块逻辑的分析又感觉到差强人意、一笔带过,今天题主尝试着解答一下这个问题,如有错误欢迎大家批评与指正!
多线程访问下数据的问题
对于多线程的数据访问,其实是跟我们的JMM内存模型有关系,在java内存模型当中的定义,所有的数据都会写到主内存当中,其它的数据需要操作时,将主内存的数据copy一份到自己线程私有的内存空间使用,操作完成后再将数据写回到主内存当中。`所以这个时候就出现了问题,我写回去了,其他的线程如果不通知的话,是不是一直感知不到。
按照下图所示:假设cpu是按照步骤顺序执行,a未加volatile关键字,请问一下步骤4和步骤6,打印出来的值是多少?很明显打印出来的都是10,因为线程2感知不到主内存的变化,所以打印的值不会收到影响。如果我将a进行volatile进行修饰了,那么步骤4和步骤6打印出来的值又会是多少了?这回打印的为10和11,因为加了volatile,所以线程能感知到主内存的变化,不会再使用线程内保存的数据,会从主内存读取。
那么问题来了,这么看好像是保证了print函数的原子性了啊。首先我们要清楚print函数肯定也不是原子性操作,第一步:加载数据到本地;第二步:打印数据。那为什么感觉像是能保证print函数的原子性了?那是因为print函数并未对a值进行二次操作,导致了print函数像是原子性函数一样,那可不就原子性了。之所有出现了非原子性的操作,不就是因为多个线程对数据进行了修改嘛。
其实答案一直在volatile关键字的定义当中,volatile保证了可见性。那么问题来了volatile保证了谁的可见性?保证的是其他的线程的可见性。保证可见性的触发时机是什么?这个动作必须要有一个指令去触发,所有的需要对这个a值进行操作的指令应该都可以。还拿上图来举例,假设步骤6什么也不做,这个时候的a对于线程2来说是多少?这个时候的值是valid,是失效的状态,所有的对于这个的值的指令操作,发现这个值失效了,会从新到主内存去加载这个值的数据。
所有的对于a有后续的数据操作的,除了读数据,print数据这种不会对数据改动的操作。其他的操作都不能保证a的‘原子性。拿a=a+1来举例:
线程1:
1.加载数据到本地线程
2.对加载的数据+1
3.将本地线程数据写回到主内存
线程2:
1.加载数据到本地线程(如果这个操作之前,数据已经写回到主内存,就能保证a的原子性,但这是不可能的)
2.对加载的数据+1
3.将本地线程数据写回到主内存
假设:线程1走到第二步v=11(+1操作),然后线程2走到了第一步a=10(load操作),这个时候在切回到线程1的第三步a=v(写回主内存)。此时切回到线程2,线程二能感知到最新数据嘛?并不能,因为这个时候已经没有数据的load操作了,虽然主内存的数据已经变更了,且线程a的数据已经valid了(别问我这个时候为什么不会触发主内存往子线程去强制推数据,俺也不知道,别人对这个关键字的定义就是保证了可见性,可没保证帮你主动把你线程内的数据给你也同步成主内存的数据,估计很多人在这个地方晕了很久,题主之前也是这样想的,你保证可见性,意思是不是主内存变化了,就帮线程把数据也给更新为最新的。NO,别人只会将你的数据更新为不可用,只有你自己主动去加载,才能获取到最新的,以题主对于上述的命令的理解,对数据进行+1的操作并不会load数据。)
小结
关于volatile为什么不能保证原子性,题主通过图文的方式,带大家简单的理解一下,希望大家能通过这种方式去了解volatile这个关键字,我们只要记住官方的定义,别人只是保证了可见性和指令顺序性。可见性是怎么实现的,可见只的是对主内存可见,对于线程来说,在你未load之前,你都是不可见的,但是只要你有任何一个对于这个数据的操作都会触发load,所以看上去像是可见了。