大致了解 volatile 的工作原理( cpu 一致性协议等),也理解它代替不了同步原语,但对它的使用场景却不怎么熟。在 web 开发中有没有可以举例说明的几个典型场景?不用就会出问题的那种?如果不用会出现什么的 error case 能具体说明就更好了。
1
lhx2008 2019-02-02 14:41:50 +08:00 via Android
延迟加载单例模式,double check + 锁
多线程库里面大量用到,不过自己写用不到 自己写可能就是一些多线程下面修改变量要考虑要不要 volatile,不过一般现在也用 atomic 包下面的 |
2
yamasa OP @lhx2008 以前看过 atomic 一些 class 的源码。刚去大致回顾了下,atomicReference 和 atomicBoolean 在内都是基于 unsafe CAS 实现的吧,不知道其他几个 atomic class 有没有特例。开发业务代码的时候,我想不到什么 case 不去直接用这几个类而要自己造轮子。当然还是应该理解其实现原理。
|
3
Raymon111111 2019-02-02 15:14:04 +08:00
理解的原理之后就知道为什么做全局计数器的时候不能用原生的 int 和 long(long 还有分段计数的风险)
|
4
yamasa OP @Raymon111111 这个我觉得多去看看 volatile 在 OS 层面的实现应该就很好理解了
|
5
ultimate010 2019-02-02 17:06:39 +08:00
web 开发最常见的就是双重检查的单例吧,好多都忘了写 volatile,是不安全的。然后还有可以作为多线程 while(!stop)这样的线程间共享的开关。
|
6
bobuick 2019-02-02 17:15:43 +08:00
题主问的是 web 开发
能用到的不多,Java 的 web 开发,其实就说一大篓子 static (此处不是真 static,只是说没状态),虽然你写的各种 class,Spring 帮你 IOC 了,可是跟 static 也没什么卵区别。 异步的一些场景的时候会用到,比如 servlet3 的 Async,如果你自己有个变量,然后自己放进 ThreadPool 内的异步跟此变量会有读取,可能就需要了。 |
7
luozic 2019-02-03 11:09:47 +08:00 via iPhone
Java io 相关的比较少,但是计算相关的大把需要考虑,特别是分布式算结果的。
|
8
yamasa OP @luozic 能否给一些更具体的实例呢?如果是出于原子性操作的考虑,为什么不是用 atomic 包装类,或者用 unsafe 提供的 cas ?还是说分布式计算更看重可见性呢?需要使用 volatile 间接完成 happens before,以保障可见性?
|
9
rim99 2019-02-03 15:34:12 +08:00 via Android
一写多读的共享变量用 volatile 修饰,可以实现轻量级的多线程及时发布。
|
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; //执行内容 } } |
11
lhx2008 2019-02-03 16:43:42 +08:00 via Android
@af463419014 要简单粗暴的话直接 enum 单例,只用一行额外代码
事实上,单例也用不到,人人都用 spring 还有启动器这种,本质也和单例问题一样,如果代码层级实现还是双重检查+锁,或者是其他机制。只用 volatile 没有太大意义。 |
12
lhx2008 2019-02-03 16:50:16 +08:00 via Android
@lhx2008
比如说,你提供代码,线程 A 已经进入 init(),执行到 start = 1,但是卡住了。线程 B 这个时候检查 inited,即使没有重排序,B 也是 init= false,所以又去执行 init()了。 |
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,也就是执行顺序被重排了 |
14
liangdu 2019-02-04 09:15:25 +08:00 via Android
说个上面没人说的,它的作用就是确保可见性,你说应用场景,基本都被都差不多是配合同步原语操作并发安全。但有一个场景很妙,采用了有限重试次数和延迟的方案优化同步性能。在 concurrentlinkedqueue 里面有一个用法,在确保可见性利用 volatile 读比 cas 写性能好的优势,延迟头尾指针写的操作。每少写一次就必须多读一次,这个代价是有利的,因为读性能比写好。
|
15
liangdu 2019-02-04 09:18:01 +08:00 via Android
@Raymon111111 分段问题在商业 vm 不存在,有针对性优化,加了专门针对它的逻辑约束。
|
16
NUT 2019-02-14 17:56:03 +08:00
这也是内存可见性的应用
典型应用: 1. cas 操作 上面的大佬都提到了。 2. 所需要的成员变量 只要求可见性,就不需要用 atomic 包的那些类。 volatile 只能保证可见性,也就是 happens-before 原则的一条规定。他无法保证线程安全。大概的原理是每个线程 读的时候强制从主内存读一遍。 |