volatile关键字使用注意事项

  • Post author:
  • Post category:其他


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个步骤:

  1. 从主存读取inc;
  2. 运算inc+1,得到结果

    inc’

  3. 将运算结果

    inc’

    赋给inc;

由于++的过程中产生了一个副本值inc’,并且这个过程是没有上锁的,所以整个对inc修改的操作并不是原子的。

不妨假设一下某个时刻 inc=7,同时有A、B两个线程同时执行了“inc++”:

  1. 线程A读取到inc=7,并运算出


    inc_a


    =inc+1=8;
  2. 线程B读取到inc=7(此时线程A还未更新主存的inc),并运算出


    inc_b


    =inc+1=8;
  3. 线程A将


    inc_a


    赋给inc,此时主存的inc最新值为8;
  4. 线程B将


    inc_b


    赋给inc,此时主存的inc最新值为8;

显然,在这种情况下,inc在两个线程中分别++后,结果就不是预期中的9了,而是8了。所以上述程序输出的结果总是<=10000。


如果我们希望保证对inc修改的过程是线程安全的呢?

如果对inc的操作只有赋值(因为基本数据类型的赋值操作是原子的),此时就是线程安全的。

如果我们对inc变量需要进行读取、再赋值(如例程里的++操作),那么我们需要保证同一时间只有一个线程会执行这个操作:

  1. 业务逻辑上同时只有一个线程会去修改inc,此时就等价于是线程安全的;
  2. 存在有多个线程同时修改inc的情况,通过上锁来保证线程安全;
  3. 使用“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一定已经被赋值了。



版权声明:本文为dawnknights原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。