volatile的意思是不稳定的,也就是敏感的。
当使用volatile关键字修饰变量时,意味着任何对此变量的操作都会在内存中进行,不会产生副本,以保证共享变量的可见性,局部阻止了指令重排的发生。
(每个线程都有独占的内存区域,如操作栈、本地变量表等等。线程本地内存保存了引用变量在堆内存中的副本,线程对变量的所有操作都在本地内存区域中进行,执行结束后再同步到堆内存中去。这里必然有一个时间差,在这个时间差内,该线程对副本的操作,对于其他线程都是不可见的。)
由此可知,在使用单例设计模式时,即使用了双检锁也不一定会拿到最新的数据。
如下示例代码在高并发场景中会存在问题:
class LazyinitDemo {
private static TransactionService service = null;
public static TransactionService getTransactionService() {
if (service == null) {
synchronized (this) {
if (service == null) {
service = new TransactionService();
}
}
}
return service;
}
}
下面进行分析:使用者在调用getTransactionService()时,有可能会得到初始化未完成的对象。这与java虚拟机的编译优化有关。对Java编译器而言,初始化TransactionService实例和将对象地址写到service字段并发原子操作,而且这两个阶段的执行顺序也是未定义的。假设某个线程执行new TransactionService()时,构造方法还未被调用,编译器仅仅为该对象分配了内存空间并设为默认值,此时若另一个线程调用getTransactionService()方法,由于service!=null,但此时service对象还没有被赋予真正有效的值,从而无法取道正确的service单例对象。这就是著名的双重检查锁定问题,对象引用在没有同步的情况下进行读操作,导致用户可能会获得未构造完成的对象。对于此问题,一种较为简单的解决方案就是用volatile关键字修饰目标属性,这样service就限制了编译器对它的相关读写操作以及禁止对它的读写操作进行指令重排,确定对象实例化之后才返回引用。
但是volatile解决的是多线程共享变量的可见性问题,类似于synchronized,但不具备synchronized的互斥性。所以对volatile变量的操作并非都具有原子性,这是一个很容易弄混的地方。
比如这个例子,一个线程对共享变量进行了10000次的i++操作,另一个线程进行10000
次i--操作,代码:
public class VolatileNotAtomic {
private static volatile long count = 0L;
private static final int NUMBER = 10000;
public static void main(String[] args) {
Thread subtractThread = new SubtractThread();
subtractThread.start();
for (int i = 0; i < NUMBER; i++) {
count++;
}
//等待减法线程结束
while (subtractThread.isAlive()) {}
System.out.print("count最后的值为:" + count);
}
private static class SubtractThread extends Thread {
public void run() {
for (int i = 0; i < NUMBER; i++) {
count--;
}
}
}
}
执行多次发现基本都不为0。如果在count++和count--两处都进行加锁才会得到0的预期。
for (int i = 0; i < MAX_VALUE; i++) {
synchronized (VolatileNotAtomic.class) {
// 在count--代码处也同样进行加锁处理
count++;
}
}
因为count++的操作其实在计算机中进行了四步:
读取count并压入操作栈顶
常量1压入操作栈顶
取出最顶部两个元素进行相加
将刚才得到的和赋值给count
因此,“volatile是轻量级的同步方式”这种说法是错误的。它只是轻量级的线程操作可见方式,并非同步方式,如果是多写场景一定会产生线程安全问题。如果是一写多读的并发场景,使用volatile修饰变量则非常合适。volatile一写多读的最典型的应用是CopyOnWriteArrayList。它在修改数据时会把整个集合的数据全部复制出来,对写操作加锁,修改完成后,再用setArray()把array指向新的集合。使用volatile可以使读线程尽快的感知array的修改,不进行指令重排,操作后即对其他线程可见。源码如下:
public class CopyOnWriteArrayList<E> {
// 集合真正存储元素的数组
private transient volatile Object[] array;
final void setArray(Object[] a) {
array = a;
}
}
在实际业务中,如何清晰的判断一写多读的场景显得尤为重要。如果不确定共享变量是否会被多个线程并发写,保险的做法是使用同步代码块来实现线程同步。另外,因为所有的操作都需要同步给内存变量,所以volatile一定会使线程的执行速度变慢,故要审慎定义和使用volatile属性。