1.volatile的作用
(1)可见性,保证线程对共享变量的修改,其他线程可见;
(2)禁止重排序,防止代码重排序,多线程情况执行出现不一致性。
2.可见性
可见性,线程A对共享变量count的修改,在其他线程(线程B、线程C)立即可见,这就是可见性。
以下代码对共享变量count的修改,线程间不具有可见性。
private int count;//共享变量
public void setCount(int count) {//线程A执行setCount方法
this.count = count;
}
public int getCount() {//线程B执行getCount方法
return this.count;
}
创建线程A对共享变量count进行赋值操作,创建线程B对共享变量count进行取值操作,理论上赋值操作执行完以后进行取值,获取到的值应该是最新,但结果并不是一定的,偶然发现count值为0。
为什么会出现这样的情况?
这是线程A的赋值操作对于线程B来不可见导致的。线程A对count进行赋值,例如赋值为10,结果存在线程A的工作内存中,没有立即更新到主内存,当线程B进行取值,主存的值没有更新还是0,所以取值为0。当然,这种情况不是必现的,但也留下不安全的因素。
如何解决?
使用volatile修饰共享变量count
其他代码不变,仅给共享变量count增加volatile修饰。
volatile作用,强制将修改后的值更新到主存,如果其他线程对该值有缓存,则会失效,那么就会从主存重新获取值;
修改代码如下:
private volatile int count;//共享变量
......
3.禁止重排序
防止代码重排序,多线程情况执行出现不一致性。
以下代码,如果b赋值先于a赋值,那么代码就没有按照编写的先后顺序执行,代码被重排了。
public void set() {
int a=1;
int b=2;
}
什么是指令重排?
CPU为了提供程序执行效率,会对指令执行顺序进行重排,以上代码,b赋值可能先于a赋值执行。
但以下代码不会涉及指令重排,因为b赋值依赖于a。
public void set() {
int a=1;
int b=a;
}
指令重排带来的问题?
单线程来说,指令重排会提高CPU执行效率,基本上没有任何问题,但对于多线程,重排会导致指令执行的不确定性。
以下代码相信大家都很熟悉,HttpManager是一个单例,提供getInstance方法获取单例,在单线程情况下,getInstance方法没有任何问题,但如果是多线程,情况就不一样了。
主要问题是 INSTANCE = new HttpManager()这行代码;
INSTANCE = new HttpManager()主要分三个步骤,
(1)开辟内存空间;
(2)初始化对象变量;
(3)INSTANCE 指向内存空间。
指令重排,导致(3)先于(2),那么这么会有什么问题呢?
线程A执行getInstance方法,判断INSTANCE 为空,获取了锁进入synchronized 代码块,执行HttpManager的实例化代码,先执行上述步骤(1),接着执行步骤(3),接着线程B执行getInstance方法,判断INSTANCE不为空(因为线程A已经实例化了INSTANCE )立马返回,在调用对象方法时发现mContext为空(因为没有执行步骤(2)),导致方法无法执行下去。
public class HttpManager {
private static HttpManager INSTANCE;
private Context mContext;
private HttpManager(Context context) {
mContext = context;
}
public static HttpManager getInstance(Context context) {
if (INSTANCE == null) {
synchronized (HttpManager.class) {
if (INSTANCE == null) {
INSTANCE = new HttpManager(context);
}
}
}
return INSTANCE;
}
}
以下代码也会因为重排序,出现问题;
线程A进入a方法判断while循环条件成立,循环执行doSomething(),线程B进入b方法,由于指令重排,语句(2)可能先于(1)执行,那么线程A判断循环条件不成立跳出循环进而执行doSomething1方法,但由于语句(1)没有执行导致context为空,那么线程A执行doSomething1方法就有可能出现异常;
private Context context;
private boolean isStop;
public void a() {
while (!isStop) {
doSomething();
}
doSomething1(context);
}
public void b() {
context = loadContext();//(1)
isStop = true;//(2)
}
如何解决重排序问题?
用volatile 修饰变量;
例子1,使用volatile 修饰INSTANCE,这样可以防止指令重排;
例子2,使用volatile 修饰isStop,volatile机制保证语句1一定在语句2之前执行完,并对后续语句可见。
public synchronized void a() {
while (!isStop) {
doSomething();
}
doSomething1(context);
}
public synchronized void b() {
context = loadContext();//(1)
isStop = true;//(2)
}
4.volatile机制和原理
加入volatile 关键字的指令,相当于多了一个lock前缀指令,可以理解为内存屏障:
(1)保证volatile指令后面的指令不会排在volatile指令的前面,前面的指令不会排到volatile指令的后面,而且保证前面指令的执行对后面指令是可见的;
以下代码,用volatile 修饰isStop ,可以保证(1)、(2)在执行(4)、(5)的时候已经执行了,结果对(4)、(5)可见;
注意,但是不能保证(1)在(2)之前执行,(4)在(5)之前执行,因为他们没有依赖关系。
private volatile boolean isStop ;
public void a() {
Person p=new Person();//(1)
Context context = loadContext();//(2)
isStop = true;//(3)
p.setName("name");//(4)
Resources resources = context.getResources();//(5)
while (!isStop) {
doSomething();
}
doSomething1(context);
}
(2)它会强制对缓存的修改刷新到主内存;
(3)如果是写操作,会导致其他线程的缓存无效;
总结:
volatile禁止重排序、可见性,不过范围比synchronized 小,比较轻量级。
以上分析有不对的地方,请指出,互相学习,谢谢哦!