已经介绍过volatile只能保证可见性和有序性(禁止指令重排序优化),既然保证了变量的可见性,有人会有这样的疑问:
volatile变量对线程立即可见,那对volatile变量的修改都能立刻反应到其他线程。
换句话说,volatile变量在各个线程中是一致的,所以volatile变量的运算在多线程下是线程安全的,也就是可以保证原子性。
但是这里面忽略了一个问题,默认运算本身是原子操作,但是实际上对volatile i++操作并不是原子操作,从主内存读->加操作->写到主内存,下面我们分析一下这个问题。
先通过一段Java案例代码对其进行相关的说明:
- package jmm;
-
- public class VolatileDemo {
-
- public static volatile int num = 0;
-
- public static void increase(){
- num ++;
- }
-
- public static void main(String[] args) throws InterruptedException {
- //1、创建10个线程
- Thread[] threads = new Thread[10];
-
- for (int i = 0; i < 10; i++) {
- //十个线程都调用普通方法
- threads[i] = new Thread(()->{
- for (int j = 0; j < 1000; j++) {
- //num ++ 操作执行1000次
- increase();
- }
- });
- threads[i].start();
- }
-
- //等待所有线程执行完成 才继续执行下面的代码
- for (Thread thread : threads) {
- thread.join();
- }
-
- System.out.println(num);
- }
- }
该代码执行后得到的结果多种多样,如:


为什么会出现这样的结果呢,接下来采取数据处理流程图进行解释说明。
在Java中,针对多线程共同处理数据操作,通常以如下方式进行:(假设2个cpu)

大家都知道num ++ 不具备原子性。
在CPU对其进行数据处理时,分为
读和写两部操作。
读:num = 0;
写:num = num + 1;
在博客缓存一致性协议(MESI)中,说到数据处理操作有如下几个步骤:
1、CPU1获取主存中,num变量信息时,将其从中拷贝副本至高速缓存中,并将其MESI状态标记为E(独享)。

2、如果此时CPU2也读取了数据,由于CPU1对其他CPU具有总线嗅探机制,当监听到被监听的数据经过BUS总线,则会将数据的状态信息变更为S(共享)状态。

3、由不同CPU去对自身独有缓存进行加锁操作,由BUS总线中的总线裁决判断哪个CPU加锁有效。
上面是数据操作的大致流程,但想过一个问题没有:
当CPU对缓存行加锁成功时,使其他CPU对该数据状态进行失效处理。
但是,如果此时数据,已经在寄存器中经过处理,只是还并未 assign(赋值) 到指定工作内存中呢?如下所示:

此时CPU1中工作内存(高速缓存)中将数据进行失效处理。
但如果
num = num + 1这个命令还在寄存器中处理,并未assign(赋值)到工作内存中,就会出现下面的情况:

CPU1中,针对num ++却在寄存器中正在被操作。CPU2获取到缓存行加锁操作,将CPU1中的缓存数据进行失效处理,但并不能将寄存器中的数据也进行失效操作。
导致
CPU1中,寄存器将数据num = num + 1进行运算操作,再执行assign(赋值)操作,写回工作内存中。
此时CPU1 工作内存对应数据并不存在,寄存器会将该信息进行写入工作内存并将其写回主存!
此时会导致CPU1和CPU2两个线程操作,都执行了一次num = 1的write(写入)操作,也就是两个线程都进行了操作,但结果却是1!!!
volatile能够保证数据变化,其他线程及时的可见性。
但不能保证数据原子性操作。