Java的面试基础问题中,经常出现并发相关的问题。比如volatile关键字,是出现频率相当高的一个问题。 如果说volatile和synchronized的区别,volatile能不能代替synchonized,不知道你是否了解?
volatile是相对于synchronized轻量级的同步关键字。它所能保证的功能比 synchonized少很多。回忆一下同步的三个要素是什么? · 原子性 · 有序性 · 可见性 对于 synchonized来说,这三个要素都是保证的,而 volatile只能保证有序性和可见性。这样会带来什么问题呢,比如我们看看下面这段代码。
public class VolatileDemo {
public static volatile int count = 0;
public static void increase() {
//为了效果明显这里增加延时
try {
Thread.sleep(10);
} catch (Exception e){}
count++;
}
public static void main(String[] args) {
for(int i = 0;i < 100; i ++) {
new Thread(new Runnable() {
@Override
public void run() {
VolatileDemo.increase();
}
}).start();
}
//延时足够长的时间等待所有线程完成
try {
Thread.sleep(2000);
} catch(Exception e) {}
System.out.println("count: " + count);
}
}
如果把volatile关键字去掉的话,这段代码输出结果肯定不是100.但如果加上 volatile呢?
$ java VolatileDemo count: 91
结果也不为100。出现这个问题的原因就要回到并发三要素了。
上面说过, volatile只满足三要素的有序和可见,不满足原子性。看上面的代码,
count++
这段代码包含三个阶段,读->改->写入内存,一个完整的并发安全操作,首要必须满足原子性,意味着当读操作发生时,应该是阻塞的,其他线程不能打断当前操作。
volatile不满足原子性,因此当线程2读count时,线程1早已把count的值读进缓存中,那么可以理解此时线程1和2中的count值是相同的。在各自修改数据后,线程1会把count值写回公共内存,虽然 volatile的可见性保证了在写入之后,其他CPU缓存中的值失效,我们以为其他线程应该会再去读最新的值,但是此时已经读取过count值的线程不会再去读取最新的count值,这导致线程2并没有在最新的值上做修改,所以导致这个问题。`
所以对于这种需要保证原子性的操作来说,用volatile关键字是不行的,得用 synchonized。 说个题外话,看下面几个操作,哪些可能不是原子操作呢?
int x,y;
long time;
x = 1; //1
y = x; //2
time = 1522048997021; //3
结果是,除了1之外2和3都不是原子操作。1和2好理解,因为单纯的读,是原子操作,读->写就不是原子操作了。 然而3为什么不是原子操作呢? 在java中,long是64位值,在某些32位系统上,对64位数据的写需要分成两次32位的写操作,因此对long的写就可能不是原子操作了。这种问题其实在面试中经常被拿来挖坑…要多注意。
回到 volatile,它的使用需要同时满足两个属性, · 对变量的写操作不依赖于当前值 · 该变量没有包含在具有其他变量的不变式中 对于第一个情形,像上面的 count++就是不满足的,虽然看起来只是一个自增操作,但实际上包含了读改写,就意味着当前值可能已经被别的线程串改了。 而对于第二个条件,字面意思不好理解,可以参考下面的代码,代码中包含一个不变式,lower < upper,即使我们把 lower和upper设定为 volatile,仍然会发生两个线程同时分别执行 setLower和 setUpper,导致区间变成类似 [3,4]的情况。
public class NumberRange {
private int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
对于上面的代码,优化的唯一方法是把两个set方法改为 synchonized。
volatile 只满足并发的可见性和有序性,对于需要保证原子性的场景则只能用 synchonized关键字。 通常用 volatile的是标志变量如
boolean volatile flag;
还有双重检查锁定的单例类实例对象中。