看到volatile,脑海里有个大概的记忆,知道它是干嘛的,但深入想一下原理,就没什么印象了。痛定思痛,一定要彻底搞懂volatile,并且深刻记住它的原理,通过一步步的抛出问题,并搜索答案的方式,深入理解volatile~
抛出问题1:volatile是什么?
回答:在java中,volatile是一个类型修饰符,被设计用来修饰被不同线程访问和修改的变量, 赋予了变量内存可见性。
问题2: volatile如何实现内存可见性?
回答:通过加入内存屏障和禁止重排序优化来实现的。对于标记了volatile的变量执行写操作时,会在写操作之后加入一条store屏障指令;执行读操作时,会在读操作之前加入一条load屏障指令。(指令read/load从主内存复制变量到当前工作内存,use/assign执行代码改变共享变量值,store/write用工作内存数据刷新主存相关内容。)
换而言之:volatile禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。
问题3: 内存屏障指令有什么用?
回答:内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。
语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。
问题4: 什么是重排序?
回答:因为jvm会对代码进行编译优化,指令会出现重排序的情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。
我们无法枚举所有的场景来规定某个线程修改的变量何时对另一个线程可见。但可以制定一些通用的规则,这就是happens-before(此规则具有传递性)。如果一个操作happens-before另一个操作,前一个操作的执行结果必须对后一个操作可见,如果不满足这个要求那就不允许这两个操作进行重排序。
重排序种类:1.编译器优化重排序,2.指令级并行重排序,3.内存系统重排序
(1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
(2)指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
(3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
问题5: 为什么volatile不能保证原子性?
回答:当volatile变量的写入语句依赖其当前值时,该操作就是非原子性的。
举例如下面代码所示:count变量被volatile修饰,线程执行方法add()中的count++非原子操作,t1和t2两条线程同时执行时,会发生读到相同值的情况,导致最后输出的总数不是5050。
public Test implements Runable{
volatile private int count = 0;
private void add(){
for (int i = 0; i < 100; i++) {
count++;
System.out.println("count = " + count);
try {
Thread.sleep(5L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
add();
}
public static void main(String[] args) {
Test test = new Test();
Thread t1 = new Thread(demo);
Thread t2 = new Thread(demo);
t1.start();
t2.start();
}
}