volatile关键字使用注意事项

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一定已经被赋值了。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值