V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yamasa
V2EX  ›  Java

Java web 开发中,有哪些需要使用 volatile 的典型场景?

  •  
  •   yamasa · 2019-02-02 14:35:29 +08:00 · 4529 次点击
    这是一个创建于 2120 天前的主题,其中的信息可能已经有所发展或是发生改变。

    大致了解 volatile 的工作原理( cpu 一致性协议等),也理解它代替不了同步原语,但对它的使用场景却不怎么熟。在 web 开发中有没有可以举例说明的几个典型场景?不用就会出问题的那种?如果不用会出现什么的 error case 能具体说明就更好了。

    16 条回复    2019-02-14 17:56:03 +08:00
    lhx2008
        1
    lhx2008  
       2019-02-02 14:41:50 +08:00 via Android
    延迟加载单例模式,double check + 锁
    多线程库里面大量用到,不过自己写用不到
    自己写可能就是一些多线程下面修改变量要考虑要不要 volatile,不过一般现在也用 atomic 包下面的
    yamasa
        2
    yamasa  
    OP
       2019-02-02 15:05:29 +08:00
    @lhx2008 以前看过 atomic 一些 class 的源码。刚去大致回顾了下,atomicReference 和 atomicBoolean 在内都是基于 unsafe CAS 实现的吧,不知道其他几个 atomic class 有没有特例。开发业务代码的时候,我想不到什么 case 不去直接用这几个类而要自己造轮子。当然还是应该理解其实现原理。
    Raymon111111
        3
    Raymon111111  
       2019-02-02 15:14:04 +08:00
    理解的原理之后就知道为什么做全局计数器的时候不能用原生的 int 和 long(long 还有分段计数的风险)
    yamasa
        4
    yamasa  
    OP
       2019-02-02 15:18:33 +08:00
    @Raymon111111 这个我觉得多去看看 volatile 在 OS 层面的实现应该就很好理解了
    ultimate010
        5
    ultimate010  
       2019-02-02 17:06:39 +08:00
    web 开发最常见的就是双重检查的单例吧,好多都忘了写 volatile,是不安全的。然后还有可以作为多线程 while(!stop)这样的线程间共享的开关。
    bobuick
        6
    bobuick  
       2019-02-02 17:15:43 +08:00
    题主问的是 web 开发
    能用到的不多,Java 的 web 开发,其实就说一大篓子 static (此处不是真 static,只是说没状态),虽然你写的各种 class,Spring 帮你 IOC 了,可是跟 static 也没什么卵区别。
    异步的一些场景的时候会用到,比如 servlet3 的 Async,如果你自己有个变量,然后自己放进 ThreadPool 内的异步跟此变量会有读取,可能就需要了。
    luozic
        7
    luozic  
       2019-02-03 11:09:47 +08:00 via iPhone
    Java io 相关的比较少,但是计算相关的大把需要考虑,特别是分布式算结果的。
    yamasa
        8
    yamasa  
    OP
       2019-02-03 12:07:07 +08:00
    @luozic 能否给一些更具体的实例呢?如果是出于原子性操作的考虑,为什么不是用 atomic 包装类,或者用 unsafe 提供的 cas ?还是说分布式计算更看重可见性呢?需要使用 volatile 间接完成 happens before,以保障可见性?
    rim99
        9
    rim99  
       2019-02-03 15:34:12 +08:00 via Android
    一写多读的共享变量用 volatile 修饰,可以实现轻量级的多线程及时发布。
    af463419014
        10
    af463419014  
       2019-02-03 15:49:56 +08:00
    @lhx2008 @ultimate010

    内部静态类单例 了解一下

    懒汉加载且线程安全,简单粗暴,锁和 volatile 都不需要

    我在实际中用过的有两种
    1.就是 @ultimate010 提到的 while(stop)
    2.防止代码重排序
    比如初始化的时候,下面这种情况
    如果 inited 参数没有 volatile 修饰,可能在 init()方法中,先执行 inited=true,再执行 start=0
    这样在 run()方法里执行 a=start 时,a 的值不等于 1
    void init(){
    start=1;
    inited=true;
    }

    void run(){
    if(inited){
    int a=start;
    //执行内容
    }
    }
    lhx2008
        11
    lhx2008  
       2019-02-03 16:43:42 +08:00 via Android
    @af463419014 要简单粗暴的话直接 enum 单例,只用一行额外代码
    事实上,单例也用不到,人人都用 spring

    还有启动器这种,本质也和单例问题一样,如果代码层级实现还是双重检查+锁,或者是其他机制。只用 volatile 没有太大意义。
    lhx2008
        12
    lhx2008  
       2019-02-03 16:50:16 +08:00 via Android
    @lhx2008
    比如说,你提供代码,线程 A 已经进入 init(),执行到 start = 1,但是卡住了。线程 B 这个时候检查 inited,即使没有重排序,B 也是 init= false,所以又去执行 init()了。
    af463419014
        13
    af463419014  
       2019-02-03 18:12:39 +08:00
    @lhx2008
    首先,enum 是饿汉单例
    双锁单例 和 内部静态类单例 则是懒汉且线程安全的,没有可比性


    另外,执行到 start=1 卡住这种不是我想表达的问题

    我想表达的是,在 init()方法中
    start=1 和 inited=true 这两行代码,有可能先执行 inited=true,再执行 start=1

    执行的顺序是:
    1.A 线程执行 inited=true
    2.B 线程判断 if(inited)成功,执行 a=start
    3.A 线程执行 start=1

    这种情况,B 线程会在认为已经初始化完成时,获取到错误是初始参数

    详细的还原代码
    https://gist.github.com/af463419014/6814e807684c46bda34349608d9f5882

    输出 out 可能等于 1,也就是 in=0,inited=1 的和
    这种情况就是先执行了 inited=1,但还未执行 in=10,也就是执行顺序被重排了
    liangdu
        14
    liangdu  
       2019-02-04 09:15:25 +08:00 via Android
    说个上面没人说的,它的作用就是确保可见性,你说应用场景,基本都被都差不多是配合同步原语操作并发安全。但有一个场景很妙,采用了有限重试次数和延迟的方案优化同步性能。在 concurrentlinkedqueue 里面有一个用法,在确保可见性利用 volatile 读比 cas 写性能好的优势,延迟头尾指针写的操作。每少写一次就必须多读一次,这个代价是有利的,因为读性能比写好。
    liangdu
        15
    liangdu  
       2019-02-04 09:18:01 +08:00 via Android
    @Raymon111111 分段问题在商业 vm 不存在,有针对性优化,加了专门针对它的逻辑约束。
    NUT
        16
    NUT  
       2019-02-14 17:56:03 +08:00
    这也是内存可见性的应用
    典型应用:
    1. cas 操作 上面的大佬都提到了。
    2. 所需要的成员变量 只要求可见性,就不需要用 atomic 包的那些类。

    volatile 只能保证可见性,也就是 happens-before 原则的一条规定。他无法保证线程安全。大概的原理是每个线程 读的时候强制从主内存读一遍。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2423 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 15:45 · PVG 23:45 · LAX 07:45 · JFK 10:45
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.