深藏不露得Java关键字 ‘’ Volatile“

前言

volatile关键字好像我们一直听人提起,但是工作中实际用到得却并不是很多。最近一直在翻相关得资料,无奈网上讲volatile的博客很多,官方的资料却很少,因为我本人还是比较喜欢先看官方的一手解读,即使一开始看不懂,留个印象,以后回过头再去再读也会有不一样的感悟。终于,在Oracle官方谈及原子操作的时候有这样一段话:

In programming, an atomic action is one that effectively happens all at once. An atomic action cannot stop in the middle: it either happens completely, or it doesn’t happen at all. No side effects of an atomic action are visible until the action is complete.

We have already seen that an increment expression, such as c++, does not describe an atomic action. Even very simple expressions can define complex actions that can decompose into other actions. However, there are actions you can specify that are atomic:

  • Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
  • Reads and writes are atomic for all variables declared volatile (including long and double variables).

Atomic actions cannot be interleaved, so they can be used without fear of thread interference. However, this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible. Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What’s more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.

关键地方已经标红,总结一下就是:

  1. 对被声明为volatile变量的读和写都是原子性的。
  2. 对被volatile声明的变量值的改变对其他线程总是立马可见,总是happens-before

如何保证内存一致性

JMM内存模型

在聊这个前我们得要先了解一下java中提出的一个模型,JMM(Java Memory Model)java内存模型。JMM只是一个概念模型,并不是实际存在的,它是由各种现实操作和出现的结果抽象出来的一种便于人们理解的一种抽象模型,通过这个模型能够很好的解释一些现象。同样,JMM内存模型的提出总是有参照,就比如现实中计算机的物理模型。

随着计算机的不断发展,人们对计算机的要求也开始越来越高,要求计算性能要提升,速度要更快等等。为了提升计算能力,多核计算机开始出现,一个Cpu不够用那我就+1,再不够用再+1。但是计算机计算往往不是单靠Cpu就够了的,他数据是存在内存中的,往往总要先从内存中把数据读取出来,拿到Cpu中计算完成,再放回内存中去。然而,由于Cpu的计算速度与内存读取IO速度之间的有着数量级上的差距。于是,在Cpu和内存两个硬件之间,设计出一种高速缓存(Cache)来作为缓冲,Cpu一开始把计算用的数据全部读取到高速缓存中,计算完了再一次性写回内存中,这样就减少了与内存的IO频率,提升了速度。

但是同时这也带来了另一个问题,如果一个值在计算中,各个Cpu都要使用,每个Cpu计算后的结果还不一样,那写回内存中的时候到底以谁为准呢?“缓存一致性协议”,就是在这种情况下被提出的。一开始,有很多"缓存一致性协议",最有名的还属MESI,这里就不再深入,有兴趣的小伙伴可以自行去网上检索。我们来画一下物理的Cpu、内存模型。

物理

有了物理模型的大体样子,java虚拟机的内存模型也就呼之欲出了。我们对照着一起看一下,是不是非常类似呢。

JMM

Volatile规则

在这样的Java内存模型下,Java对于Volitale有一些特殊的规则,来保证volatile能够做到轻量级的同步。

  1. volitale变量的改变总是立马对其他线程可见的。

    在JMM内存模型中,每个线程计算开始前都会把volatile变量从主内存拷贝一下,放在自己的工作内存副本当中。一旦其中任意一个线程改变了这个volatile的值,必须立马把改变后的值刷新回到主内存中。其他线程发现值被变更后,必须重新去主内存中拿到最新的值放到自己的工作内存中,来保证上一个线程的更新对其他线程立马可见。

    那么,其他线程是怎么知道这个volatile修饰的变量发生了改变的呢?

    嗅探机制”。 可以这么理解,整个内存就好比一条数据总线,一个河流,数据流在这条总线上不断的流动交互,波涛汹涌。每个线程都有一根筋搭在这条河流上,监听着volatile字段的变化,一旦监听到了改变,立马通知自己线程去重新获取刷新线程本身的工作内存。

    所以经常会有这样一个问题,既然volatile这么好用,是不是用的越多越好呢?

    每个线程都过来嗅探一下,嗅探也是需要消耗资源的,如果越来越多的线程都想来嗅探数据总线,就会出现一种"总线风暴"的现象,带宽不断提高,甚至能够到达峰值。

  2. volatile禁止指令重排。

    指令重排是个什么意思呢?简单的说,就是你看到并不一定是实际发生。并不是你写代码的时候,先写的代码就一定先执行,后写的代码一定后执行。JVM为了提高效率,可以在不影响最终结果的情况下,自由优化代码的执行顺序。

    但是volatile有一个内存屏障,对于volatile变量的读和写,都有内存屏障的出现来保证volatile在内存中语义的正确性。

    • 在每个volatile的写操作前面插入一个StoreStore屏障。

      目的是,禁止上面普通的写操作和下面的volatile写重排序

    • 在每个volatile的写操作后面插入一个StoreLoad屏障。

      目的是,防止上面的写可能与下面出些的读或写重排序。

    • 在每个volatile的读操作后面插入一个LoadLoad屏障。

      目的是,禁止下面得所有普通读操作与上面得volatile读重排序

    • 在每个volatie得读操作后面插入一个LoadStore屏障。

      目的是,禁止下面得普通写操作和上面得volatile读重排序

      但是volatile有一个内存屏障,对于volatile变量的读和写,都有内存屏障的出现来保证volatile在内存中语义的正确性。

    由这种严格限制得重排序规则和先行发生原则得约束下,我们便可以知道:对一个volatile变量得写操作先行发生于后面对这个变量得读操作。

Volatile使用

明白了volatile的原理,如何去使用它呢?

这个时候我们可以去学习一下看一下java源码哪里用到了volatile。最典型的莫过于Lock了。

在Java Concurrent包下面的ReentrantLock中加锁的时候

/**
 * Fair version of tryAcquire.  Don't grant access unless
 * recursive call or no waiters or is first.
 */
protected final boolean tryAcquire(int acquires) {
	//获取当前线程
    final Thread current = Thread.currentThread();
    //获取当前状态值
    int c = getState();
    //如果状态值是0,表示未被锁住
    if (c == 0) {
    //如果在当前线程之前没有其他线程申请锁了
    //并且
    //CAS(比较并交换成功)
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            //设置当前线程未持有锁的线程
            setExclusiveOwnerThread(current);
            //返回成功
            return true;
        }
    }
    //如果状态值不是0,校验是否当前线程就是持有锁的线程
    else if (current == getExclusiveOwnerThread()) {
    	//是的话,计数增加
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //返回加锁失败
    return false;
}

Lock加锁有公平锁和非公平锁之分,这里拿公平锁版本做介绍。tryAcquire尝试获取锁,其中获取状态值,这个状态值我们点进去看一下,就是用volatile修饰的一个int型的变量值了。

/**
 * The synchronization state.
 */
private volatile int state;

/**
 * Returns the current value of synchronization state.
 * This operation has memory semantics of a {@code volatile} read.
 * @return current state value
 */
protected final int getState() {
    return state;

这样就很好理解了,未加锁状态是0,某一个线程持有锁就改变这个状态值,其他线程能够立马看到状态值的改变,就能知道对象是否被加锁,设计的十分巧妙。

Volatile的坑

Volatile使用

volatile有坑,坑在哪里呢?

volatile保证了对值得变更对所有线程是立马可见得。就有人认为,基于volatile的计算在多线程下是安全的。“既然,volatile在各个线程中值都是一样的,例如初始值0,那我在A线程中加1之后,B,C线程立马拿到的是1,B再加1之后,A、C立马看到的是2,这样不是没有问题嘛”。想法是很美好的,但是现实却是很残酷的。下载

原因就是,volatile只保证了内存可见性一致,不能保证操作的原子性。

就比如代码中的一句a++,编译到字节码文件中却是由好几条字节码语句组成的。volatile能够保证你看到的值一定是正确的,但是从你看到值,到开始操作相加,最终结果返回回填,这中间还有大量的时间,它不能保证在这一整个操作中,volatile值一直没有发生过变化。因此,这种情况还是需要我们对一整个流程经行加锁,保证从看到值,到计算,到结果返回整个流程没有干扰才行。

结束

好了,说了这么多相信你对volatile也有一定的了解了。对比上一篇的Synchronized,在功能上Syncronized似乎更强大一点,Synchronized的互斥性能够保证在整个临界区域内的原子性,但是在性能上稍逊色一点。volatile你就可以直接看作对一个普通变量的读取,不用他来做计算,用来作为比对的话,在性能上和伸缩性上有很大的优势。

就好像《葵花宝典》和《七伤拳》,一个欲练此功必先自宫,一个一练七伤七者皆伤,怎么选就看你们啦!

比武

一点碎碎念
最近一周发生了很多事情,周四刚开发完准备发版,突然说公司没了,接下来就是处理各种善后的事情。老板人也不错,直接跟我们沟通了现在公司目前的情况,给大家发了补偿。从0到1搞起来的东西,说没就没了,纵有万分不舍,也只能江湖再见。
接下来本来想先休息一下,但是工作还没有着落,又是紧锣密鼓的跟随节奏打算找个新工作把工作定下来,有些事情真是一言难尽啊。
本来构思写了一半了的东西,上次撂下了,今天也是匆忙把它写完,如有不足,或是错误,欢迎评论指出!要是点个赞,那就更好了呢!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值