前言
volatile在多线程开发中是可以经常看到的变量修饰符,本文主要是比较浅显的介绍volatile的作用。
在开始之前多线程并发编码往往需要考虑这几个方面:
- 原子性:一段代码,按顺序完整执行,执行过程中不能插入其他的操作。
- 可见性:一个线程修改了共享变量或者副本变量其他有关的线程能立即获得反馈。
- 顺序性:程序执行的顺序按照代码的先后顺序执行。
volatile修饰符是用来提高可见性和顺序性的。
volatile修饰符不保证原子性。
可见性
JAVA内存模型
简化:
JVM在启动一个新的线程时,即调用Thread.start()会为此线程开辟一个工作空间(虚拟机栈)这个内存空间会copy Thread对象的属性到工作空间中这里会进行值Copy的是基本类型对象如boolean,int等,复杂对象仅仅会复制引用不会复制实例。这个模型就会有一个问题,线程A和线程B都是在读自己的副本变量,如果另外一个线程修改了共享变量A,对于线程A和线程B都是没有感知的。
案例
public class StoppableTask extends Thread {
private boolean pleaseStop;
private StringBuffer stringBuffer = new StringBuffer();
public void run() {
while (!pleaseStop) {
//System.out.println(i);
}
System.out.println("stop");
}
public void shutdown() {
pleaseStop = true;
}
public static void main(String[] args) {
StoppableTask task = new StoppableTask();
task.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
task.shutdown();
}
}
运行这段代码可以发现,即使我们调用了task.shutdown方法,子线程还是在继续执行。这是因为我们仅仅改变的是主内存中的pleaseStop的值,线程工作空间的pleaseStop的值依旧是false针对这个问题我们只要用volatile修饰下pleaseStop即可,这样每次子线程去读取pleaseStop值时都会去主线程中读取。
修改这里
private volatile boolean pleaseStop;
做这个实验时大家要注意 System.out.println千万不要出现在你的while循环里面,因为println方法里面有synchronized修饰符,synchronized在进行加锁时也会去主内存中同步共享变量。
至于副本变量何时会主动与主内存变量同步,这里的话只要我们在子线程中修改副本变量,注意是要在子线程的运行环境中修改,会自动同步到主内存中。
顺序性
指令重排
指令重排指的是为了提高CPU运行效率,代码执行可能不按照顺序执行但是会保证单线程情况下输出结果的一致。
示例如下:
int a = 3; //语句1
int b = 4; //语句2
int c = a + b; //语句3
int d = c + a; //语句4
int e = a * a; //语句5
上面的代码如果按顺序执行应该是
但是我们发现语句5在语句1完成后就可以进行运算,并且由于所需的参数a刚好在寄存器的栈顶,我们是不是可以把语句5提前运算以增加效率。结果运行顺序就变成如下:
以上在单线程下是没有问题的,但是如果在多线程的情况下指令重排可能就会产生错误的结果。示例如下:
//线程A
configure = loadConfigure("xxxxx.xml"); //1
isInit = true; //2
//线程B
while(!isInit) continue;
initialize(configure);
如果在这个案例中 语句2提前到语句1前面执行了,那么可能会产生空指针的异常情况,因为在这个时候,configure并没有初始化完成,但是线程B已经在运行initialize的方法了。
针对于这种情况,我们只要将isInit使用volatile进行修饰即可。因为volatile变量会保证在volatile变量之前声明的语句会早于当前volatile的位置执行,volatile变量之后声明的的语句不会早于当前的volatile位置执行。
代码重排可以发生在编译期间,运行期间
案例
volatile关于代码顺序的问题有一个比较经典的案例,就是我们的单例模式的初始化,代码如下:
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
在这段代码中,我们看到了Singleton添加了volatile修饰符,这里是处于什么考虑呢?
因为 instance = new Singleton() 在进行创建实例的时候要经过三个步骤
- 申请instance内存空间。
- 初始化instance实例。
- 将instance的引用指向instance的实例。
这三个步骤在字节码层面是多条语句,可以发生指令重排那么可能会产生下面这种情况:
重排的结果如下:
- 申请instance内存空间。
- 将instance的引用指向instance的实例。
- 初始化instance实例。
在单线程下没有问题,但是在多线程下假设线程A进行到第3步时中断,而此时刚好线程B执行到instance是否为空的判断,这时instance引用指向确实不为null,所以会返回instance所指向的实例,但是该实例并未经过初始化,所以强行使用的话就会产生程序异常。
参考: