volatile, 用更低的代价替代同步

为什么 使用volatile比同步代价更低?
同步的代价, 主要由其覆盖范围决定, 如果可以降低同步的覆盖范围, 则可以大幅提升程序性能. 

而volatile的覆盖范围仅仅变量级别的. 因此它的同步代价很低.

volatile原理是什么?
volatile的语义, 其实是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我.(工作内存详见java内存模型)

因此, 当多核或多线程在访问该变量时, 都将直接
操作 主存, 这从本质上, 做到了变量共享.

volatile的有什么优势?
1, 更大的程序吞吐量
2, 更少的代码实现多线程
3, 程序的伸缩性较好
4, 比较好理解, 无需太高的学习成本

volatile有什么劣势?
1, 容易出问题
2, 比较难设计




volatile运算存在脏数据问题

volatile仅仅能保证变量可见性, 无法保证原子性.

volatile的race condition示例:

public class TestRaceCondition {
    private volatile int i = 0;
   
    public void increase() {
       i++;
    }

    public int getValue() {
       return i;
    }
}

当多线程执行increase方法时, 是否能保证它的值会是线性递增的呢?
答案是否定的.

原因:
这里的increase方法, 执行的操作是i++, 即 i = i + 1;
针对i = i + 1, 在多线程中的运算, 本身需要改变i的值.
如果, 在i已从内存中取到最新值, 但未与1进行运算, 此时其他线程已数次将运算结果赋值给i.
则当前线程结束时, 之前的数次运算结果都将被覆盖.

即, 执行100次increase, 可能结果是 < 100.
一般来说, 这种情况需要较高的压力与并发情况下, 才会出现.

如何避免这种情况?
解决以上问题的方法:
一种是 操作时, 加上同步.
这种方法, 无疑将大大降低程序性能, 且违背了volatile的初衷.

第二种方式是, 使用硬件原语(CAS), 实现非阻塞算法
从CPU原语上,  支持变量级别的低开销同步.




CPU原语-比较并交换(CompareAndSet),实现非阻塞算法

什么是CAS?
cas是现代CPU提供给并发程序使用的原语操作. 不同的CPU有不同的使用规范.

在 Intel 处理器中,比较并交换通过指令的 cmpxchg 系列实现。
PowerPC 处理器有一对名为“加载并保留”和“条件存储”的指令,它们实现相同的目地;
MIPS 与 PowerPC 处理器相似,除了第一个指令称为“加载链接”。


CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)

什么是非阻塞算法?
一个线程的失败或挂起不应该影响其他线程的失败或挂起.这类算法称之为非阻塞(nonblocking)算法

对比阻塞算法:
如果有一类并发操作, 其中一个线程优先得到对象监视器的锁, 当其他线程到达同步边界时, 就会被阻塞.
直到前一个线程释放掉锁后, 才可以继续竞争对象锁.(当然,这里的竞争也可是公平的, 按先来后到的次序)

CAS 原理:

我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

CAS使用示例(jdk 1.5 并发包 AtomicInteger类分析:)

  

 /**
     * Atomically sets to the given value and returns the old value.
     *
     * @param newValue the new value
     * @return the previous value
     */
    public final int getAndSet(int newValue) {
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
 


这个方法是, AtomicInteger类的常用方法, 作用是, 将变量设置为指定值, 并返回设置前的值.
它利用了cpu原语compareAndSet来保障值的唯一性.

另, AtomicInteger类中, 其他的实用方法, 也是基于同样的实现方式.
比如 getAndIncrement, getAndDecrement, getAndAdd等等.


CAS语义上存在的 "
ABA 问题"

什么是ABA问题?
假设, 第一次读取V地址的A值, 然后通过CAS来判断V地址的值是否仍旧为A, 如果是, 就将B的值写入V地址,覆盖A值.

但是, 语义上, 有一个漏洞, 当第一次读取V的A值, 此时, 内存V的值变为B值, 然后在未执行CAS前, 又变回了A值.
此时, CAS再执行时, 会判断其正确的, 并进行赋值.

这种判断值的方式来断定内存是否被修改过, 针对某些问题, 是不适用的.

为了解决这种问题, jdk 1.5并发包提供了AtomicStampedReference(有标记的原子引用)类, 通过控制变量值的版本来保证CAS正确性.

其实, 大部分通过值的变化来CAS, 已经够用了.



jdk1.5原子包介绍(基于volatile)

包的特色:
1, 普通原子数值类型AtomicInteger, AtomicLong提供一些原子操作的加减运算.

2, 使用了解决脏数据问题的经典模式-"比对后设定", 即 查看主存中数据是否与预期提供的值一致,如果一致,才更新.

3, 使用AtomicReference可以实现对所有对象的原子引用及赋值.包括Double与Float,
但不包括对其的计算.浮点的计算,只能依靠同步关键字或Lock接口来实现了.

4, 对数组元素里的对象,符合以上特点的, 也可采用原子操作.包里提供了一些数组原子操作类
AtomicIntegerArray, AtomicLongArray等等.

5, 大幅度提升系统吞吐量及性能.

具体使用, 详解java doc.

评论
wf_chn 2007-12-13
这篇文章我喜欢,虽然看不大懂

我想问问在jdk1.4里volatile有没有作用
我在看java thread programming 反正第一个例子就跑不了
Godlikeme 2007-08-29
dennis_zane 写道
Godlikeme 写道
int操作是原子的是什么意思呢?

jvm保证int类型的读写原子性,正因为是基本数据单元才能有这样的保证。
不加volatile,不保证读写的值是最新的。
参见jls http://java.sun.com/docs/books/jls/


基本数据类型,除double,long的64位之外才有保证吧,jvm将64位的读和写划分位两个32位的操作,因此只有声明volatile或者加锁才能保证long、double读和写的原子性。
我最近也在读Java Concurrency in Practice,volatile的概念在C/C++中有比较充分的解释。

对的,我们说得是一个意思。
最近也在看,有机会多交流。
galaxystar 2007-08-29
不扯远了, C++做基础设施, 我个人感觉是最好的, 比如搜索引擎, 分布式cache之类的.
因为跟操作系统能接触得更多.
java适合开发应用软件, 那也是我看好的.
七猫 2007-08-29
开发多线程的话,如果C++公司或者小组有了一整套的库,那也跟JAVA开发效率会差不多,但C++根本没有线程的概念,而且还有个据说是C++的牛人写了篇文章:为什么线程不应该在C++中实现。


其实多线程也就那么回事:
thread
atomic
mutex(sleep or not sleep)
tls
基本上每种语言差不多,只不过JAVA,C#多了一些更高层的封装,确实有时候简化了一些开发。
七猫 2007-08-29
有关java的C++的运行效率和开发效率的比较感觉是已经成为一个哲学问题了。而且永远不会结束,争论的人大部分也只熟悉其中的一门语言,而且很多人还有很多偏见,根本对其他人的意见无动于衷,特别是C++开发者,甚至还会认为自己是最聪明的程序员。而在我看来,写操作系统的程序员才是最聪明的。因为他们对架构和语言都要非常熟悉,对硬件也要很熟悉。

拿一些代码片断或一些小的项目来比较也没有什么实际意义。举个简单的例子,用java也写一个ultraedit或者写一个heroes2,用C++写个spring的架构,这样比较我觉得才有实际价值。100行,200行的代码,最多只能叫玩具而已。

假如真的要争论效率,我觉得最简单的方法就是说说他们在CPU中到底是怎么运行的,为什么会更快。可惜没有太多人能达到汇编级,编译器级的解释。

至于开发效率,C++的开发效率有目共睹,而C++的委员会又一直在搞什么八辈子用不到的语言扩展,对库的开发很少关心(我前段时间刚跟人家争过http://blog.csdn.net/pongba/archive/2007/08/04/1726031.aspx,http://blog.csdn.net/pongba/archive/2007/08/08/1732055.aspx)

我觉得不管用什么语言,能最快最好完成客户的需要才是真的。假如我要开发一个有GUI的客户端,那我基本上不会用JAVA来开发(肯定用C++,delphi,bcb,再不行C#也成),假如要开发WEB,我也不会去用CGI去开发(虽然以前我也写过,但那个痛苦实在没法说)。
galaxystar 2007-08-28
有一点,
值得提一下, java开发多线程普遍比c++快, 在jcp的努力下, 多核平台下运行效率已与C++也不相上下.
七猫 2007-08-28
我总感觉到JAVA中的这个关键字没有太多实质性的作用,没必要有这么多的关注,正如C++中的这个关键字现在也很少会用到,编译器会帮你做一些判断。register在大部分情况下也用不到了。我觉得在JAVA里这个参数的作用也只是让JAVA在操作这个的时候不把他放到JVM的什么其他地方,而是每次直接操作这块内存。
七猫 2007-08-28
C++中
在单CPU中,对一个volatile整数(已对齐)的操作,++,+=是多线程安全的,
在多CPU中,对一个volatile整数(已对齐)的操作,++,+=都不安全,汇编中只有xchg是多CPU安全的,其他的操作需要加前缀lock来锁定总线。

没看过java内部的volatile的实现。不知道他是怎么做的。

C++的volatile感觉是相对于register关键字而来的(这个关键字允许你的一个变量只存在于寄存器中,特别是一些临时变量,这在优化中很常见)

volatile假如他的实现是一个atom,那肯定是多线程安全的,假如有个lock,不管是spinlock还是waitlock,那效率还不如atomic

http://www.ibm.com/developerworks/java/library/j-jtp06197.html
这篇文章似乎提及了java中的这个volatile,提到了下面两种情况下才可能线程安全:
Writes to the variable do not depend on its current value. (++,--都不行)
The variable does not participate in invariants with other variables. (+=也不行)
从http://www.ibm.com/developerworks/java/library/j-jtp06197.html这篇文章看来,我认为他不能代替其他的lock,最多做为一个flag,staus这样的标志来使用。
galaxystar 2007-08-28
七猫 写道
我不知道java里的volatile内是什么含义,在C++里面,仅仅表示这个东东在内存,在内存一定能同步吗?不能,ia32的手册说了,多CPU访问要加lock 前缀的。

volatile就是羽量级的lock.
七猫 2007-08-28
我不知道java里的volatile内是什么含义,在C++里面,仅仅表示这个东东在内存,在内存一定能同步吗?不能,ia32的手册说了,多CPU访问要加lock 前缀的。
dennis_zane 2007-08-24
Godlikeme 写道
int操作是原子的是什么意思呢?

jvm保证int类型的读写原子性,正因为是基本数据单元才能有这样的保证。
不加volatile,不保证读写的值是最新的。
参见jls http://java.sun.com/docs/books/jls/


基本数据类型,除double,long的64位之外才有保证吧,jvm将64位的读和写划分位两个32位的操作,因此只有声明volatile或者加锁才能保证long、double读和写的原子性。
我最近也在读Java Concurrency in Practice,volatile的概念在C/C++中有比较充分的解释。
galaxystar 2007-08-20
歆渊说得不错, 一般 cache 是jvm的work memory,
线程变量拷贝, 优先存放在 cache里
歆渊 2007-08-20
ueseu 写道
来自http://www.laogu.com/wz_692.htm

引用

volatile的本意是“易变的”

由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:

static int i=0;

int main(void)
{
...
while (1)
{
if (i) dosomething();
}
}

/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}

程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。

一般说来,volatile用在如下的几个地方:

1、中断服务程序中修改的供其它程序检测的变量需要加volatile;

2、多任务环境下各任务间共享的标志应该加volatile;

3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。

这是C++的volatile的文章, 看起来应该还是比较清楚的

里面提的"寄存器"就是JAVA的MAIN MEMERY, "内存"就是WORKING MEMERY


好像应该是反过来吧:

"寄存器" = Working Memory
"内存" = Main Memory

实际上多CPU/核心的情况下, C/C++ 也有类似的问题, 主要是CPU核心的内置缓存和真正的系统内存间同步问题. 只不过C/C++上不同平台有不同的控制接口, 而Java是统一的内存模型.
Godlikeme 2007-08-20
int操作是原子的是什么意思呢?

jvm保证int类型的读写原子性,正因为是基本数据单元才能有这样的保证。
不加volatile,不保证读写的值是最新的。
参见jls http://java.sun.com/docs/books/jls/
likeblood 2007-08-20
说实话,在看到这篇文章以前,我一直认为int之类的4字节原始类型的操作都是原子的,甚至不必加什么volatile,看来是不行的,我觉得这该算是jvm的一个缺陷了吧,对于32位机器来说,4字节正好就是一个基本的数据单元,连这样的单元都不保证同步。不过想想也许这样设计也是出于当初把Hashtable改写为HashMap的原因一样吧
Godlikeme 2007-08-19
c++中volatile和java中volatile概念基本上已经不能在类比了。
ueseu 2007-08-19
来自http://www.laogu.com/wz_692.htm

引用

volatile的本意是“易变的”

由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:

static int i=0;

int main(void)
{
...
while (1)
{
if (i) dosomething();
}
}

/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}

程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。

一般说来,volatile用在如下的几个地方:

1、中断服务程序中修改的供其它程序检测的变量需要加volatile;

2、多任务环境下各任务间共享的标志应该加volatile;

3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。

这是C++的volatile的文章, 看起来应该还是比较清楚的

里面提的"寄存器"就是JAVA的MAIN MEMERY, "内存"就是WORKING MEMERY
schweigen 2007-08-10
volatile 可以安全的替代锁的两个条件:
1、对变量的写操作不依赖于当前值。
2、该变量没有包含在具有其他变量的不变式中。

针对条件1,i++ i-- 这样的操作是不能用volatile 替代
针对条件2,if (volatileVar > num) 这样的形式也不能用volatile 替代锁
javavsnet 2007-08-08
我在一台双核的机器上做了测试,的确发生了脏读。看来volatile不能满足++操作的原子性。我想原因是++操作实际是read, update, write三个动作,而volatile只保证read, update, write 本身是原子操作。
歆渊 2007-08-08
嗯, 单核情况下, 要凑巧在 i++ 中间切换线程上下文的概率太低了, 而且说不定 JIT编译器, OS, 流水线, 或者 CPU微指令 里一个或者多个层次上就会把对同一个内存地址的 读后马上写 的操作优化到没法切换线程上下文的微过程里去.
发表评论

提醒: 该博客已发表在公共论坛,博客所有留言会成为论坛回贴,留言请注意遵守论坛发贴规则

您还没有登录,请登录后发表评论

galaxystar
搜索本博客
存档
最新评论