多线程正常执行的时候,我们说的共享变量,其实每个线程都会在线程内部创建变量的一个副本,线程中操作的其实就是这个副本变量,运算结束后,会同步副本变量与原始变量。
synchronized:同步方法或者代码块,之间的操作保证线程安全,原子性操作;
volatile :保证变量的可见性,但是不保证原子性。
volatile和非volatile:
volatile:主要在于保证变量的可见性。
volatile能不用就不用吧,这玩意感觉有些复杂了。
有三个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障(volatile修饰的变量)这句指令时,在它前面的操作已经全部完成,并且结果对后可见;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他线程中对应的缓存行无效。
而非volatile:不会保证变量可见性,因此其他线程读取的数值就可能不是最新的。
volatile的使用场景:适合条件:
1.对变量的写操作不依赖于当前值。
2.该变量没有包含在具有其他变量的不变式中。
模式 #1:状态标志
//flage标志,true关闭,false没有关闭
volatile boolean flage;
public void shutdown() {
flage = true;
}
public void doWork() {
while (!flage ) {
// do stuff
}
}
上面代码,如果没有volatile标识,线程1调用shutdown,缓存中flage=true,再将缓存和主内存同步,但是这些不是原子操作的,
这时另外的线程执行dowork方法,取得的值还是false。加上volatile关键词后,一旦flage=true后,会立即同步主内存的值(变量的可见性),这样dowork取得的值就是最新的true,从而保证了变量的原子性。
模式 2:一次性安全发布
在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。
private volatile static Singleton instance;
public static Singleton getInstance(){
//第一次null检查
if(instance == null){
synchronized(Singleton.class) { //1
//第二次null检查
if(instance == null){ //2
instance = new Singleton();//3
}
}
}
return instance;
}
上述代码,在instance未使用volatile关键词的情况下分析:
线路a获得运行,第一次检查结果为null,然后进入同步块,第二次检查为null,开始构建对象,并返回这个实例。
在返回这个实例化对象之前,线程b取得运行,检查instance为非null,会直接到最末一句,返回了线程a构建的那个实例。
现在最主要的在于这一句代码:instance = new Singleton();这一步会很为两步来操作,并且是没有先后顺序的,1在堆内存中开辟new Singleton()内存并初始化;2在栈内存中instance指向堆内存地址;如果先执行的是第2步,然后线程b执行了,返回的是非null值,但是这个对象并没有初始化。
现在加上volatile修改词之后,在标记3处:instance = new Singleton(); 1是禁止指令重新排序,此代码先执行对象初始化,再将地址传递给引用,所以不存在instance为一个未初始化的变量;2是instance引用改变后,会立即写入主内存,对其他线程者是立即可见的。