volatile保证可见性的代码演示:
image.png
demo2
package jucTest;
public class VolatileTest {
boolean stop = false;//如果不加关键字,程序将一直循环
// volatile boolean stop = false;
public static void main(String[] args) throws Exception{
VolatileTest v = new VolatileTest();
Thread ta = new Thread(()->v.execute());
ta.start();
Thread.sleep(2000);
Thread tb = new Thread(()->v.shutdown());
tb.start();
}
public void execute(){
while(!stop){
String a = "a";
// System.out.print("");
}
}
public void shutdown(){
System.out.println("do stop");
stop = true;
}
}
因为在多线程中,ABC 3个线程拿到主内存的数据s后,可能出现,A改了s的值正要刷回主内存的时候线程被挂起,这时候B线程改了s的值,当A线程再次开启时候还没来得及被通知就已经把自己改后的数据注入了,这时候就存在一个数据的丢失问题.
如何在不使用synchroniza的情况下保证int类数据的原子性呢? java.until.concurrent.atomic.AtomcInteger,它提供了一个保证原子性的int类的数据类AtomicInteger,它可以保证数据的原子性,可以当作int值来使用,自身带有操作数方法 如:AtomicInteger ai=new AtomicInteger(4); ai就是一个值为4的数据,如果括号内不写的话 默认为0
指令重排: 在计算机执行指令的顺序在经过程序编译器编译之后形成一个指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
image.png
volatile禁止了指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象
先了解一个概念,内存屏障
(Memory Barier)又称内存栅栏,是一个CPU指令
,内存屏障可以禁止特定类型处理器的重排序
,从而让程序按我们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:
保证特定操作的执行顺序
。影响某些数据(或则是某条指令的执行结果)的内存可见性
。编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。
而插入一条Memory Barrier会告诉编译器和CPU:
①不管什么指令都不能和这条Memory Barrier指令重排序。
②Memory Barrier所做的另外一件事是强制刷出各种CPU cache
,如一个Write-Barrier(写入屏障)将刷出所有在Barrier之前写入 cache 的数据
,因此,任何CPU上的线程都能读取到这些数据的最新版本。
volatile就是基于Memory Barier 如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,内存屏障的插入就可以保证:
volatile的读写屏障图
image.png
工作内存与主内存同步延迟现象导致的可见性问题
可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。
read (读取) 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load (载入) 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use (使用) 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。
assign (赋值) 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store (存储) 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后write操作使用。
lock (锁定) 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock (解锁) 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
write (写入) 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。