volatile不能保证原子性
众所周知,volatile一般用于修饰会被多个线程使用的变量。
假设我们有一个公共变量inc
private static volatile int inc;
要注意的是,volatile保证的变量i的可见性,也就是各个线程在读取inc时,都能读取到inc变量在主存上的最新值(换句话说,避免“脏读”)。
但是,volatile是不能保证在多个线程同时修改inc时的原子性。我们通过一段程序来验证:
public static void main(String[] args) {
inc = 0;
for(int i=0;i<10000;i++) {
Thread t = new Thread() {
public void run() {
try {
Thread.sleep(10);
}
catch(Exception e) {}
inc++;
}
};
t.start();
}
//主线程sleep的时间稍微长一点,保证10000个线程都能跑完
Thread.sleep(3000);
System.out.println(String.format("thread[%s]:total[%s]", "main",inc));
}
这段程序就是开启10000个线程,每个线程都对inc做一次++操作(注意:主线程一定要等所有子线程运行结束再打印出结果)。
如果每个++操作都是原子的,那么inc的最终结果应该10000。但是我们多运行几次下来的结果,inc累加的结果明显是<=10000。
java基本数据类型的赋值操作是原子的(这里不展开讲,有兴趣可以自行查找java内存模型相关资料),但是对inc的++操作其实就是“inc=inc+1”,并不是原子的,其中,包含3个步骤:
- 从主存读取inc;
-
运算inc+1,得到结果
inc’
; -
将运算结果
inc’
赋给inc;
由于++的过程中产生了一个副本值inc’,并且这个过程是没有上锁的,所以整个对inc修改的操作并不是原子的。
不妨假设一下某个时刻 inc=7,同时有A、B两个线程同时执行了“inc++”:
-
线程A读取到inc=7,并运算出
inc_a
=inc+1=8; -
线程B读取到inc=7(此时线程A还未更新主存的inc),并运算出
inc_b
=inc+1=8; -
线程A将
inc_a
赋给inc,此时主存的inc最新值为8; -
线程B将
inc_b
赋给inc,此时主存的inc最新值为8;
显然,在这种情况下,inc在两个线程中分别++后,结果就不是预期中的9了,而是8了。所以上述程序输出的结果总是<=10000。
如果我们希望保证对inc修改的过程是线程安全的呢?
如果对inc的操作只有赋值(因为基本数据类型的赋值操作是原子的),此时就是线程安全的。
如果我们对inc变量需要进行读取、再赋值(如例程里的++操作),那么我们需要保证同一时间只有一个线程会执行这个操作:
- 业务逻辑上同时只有一个线程会去修改inc,此时就等价于是线程安全的;
- 存在有多个线程同时修改inc的情况,通过上锁来保证线程安全;
- 使用“AtomicInteger”来代替“volatile int“”,此时对inc自增时不用加锁 ,利用CAS实现了原子性(这可以实现无锁编程);
volatile禁用指令重排
以下代码也就是经典的基于双重检查锁的单例模式,懒汉式实现:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton .class) {
if (singleton == null) {
singleton = new Singleton ();
}
}
}
return singleton;
}
}
通过volatile禁用指令重排的机制,保证了singleton=new singleton()过程的线程安全。
我们假设没有给instance加上volatile,也就是存在指令重排的情况下,有两个线程T1、T2同时调用了getInstance(),此时假设T1进入synchronized代码块并调用了new Singeton(),但是由于指令重排,已经给singleton分配了内存但仍未初始化这个类;这样可能会导致一个什么样的结果?
就是T2在第一个if判断时,就有可能判断为false(因为已经分配了内存,singleton已经不为空了),从而直接return,然而此时初始化仍未完成,就会产生问题了。
网上一些比较老的文章可能会说到这种写法出于jvm内存模型的缺陷,仍然有问题。事实上在jdk1.5以上,基本可以放心使用。
原因其实就是volatile在后续新版本jdk中的语义得到强化,可以实现更严格的“禁用指令重排”。假设我们定义了两个volatile修饰的变量:
private static volatile int num = 0;
private static volatile boolean flag = false;
private static void init() {
num = 1;
flag = true;
}
private static boolean isInited() {
return flag;
}
语义强化后的volatile可以防止init()方法的两个赋值语句发生指令重排,也就是当isInited()返回true时,num一定已经被赋值了。