在之前的文章中java中的volatile关键字的作用_ErwinNakajima的博客-CSDN博客_java中的volatile关键字的作用
已经介绍过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
能够保证数据变化,其他线程及时
的可见性
。
但不能保证数据原子性
操作。