本文的copyleft归gfree.wind@gmail.com所有,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
======================================================================================================
关于volatile的说明,这是一个老生常谈的问题。volatile的定义很简单,将其理解为易变的,防止编译器对其进行优化。那么其用途一般有以下三种:
1. 外部设备寄存器映射后的内存——因为外部设备寄存器可能会由于外部设备的状态变化而改变,因此映射后的内存需要注明为volatile的;
2. 多线程或异步访问的全局变量;
3. 嵌入式汇编——防止编译器对其优化;
 
这三种用途中的第一种和第三种一般都没有什么疑问。但是关于第二个用途,经常有朋友对其有误解。
 
 
首先说一下,什么时候情况下全局变量需要有volatile修饰呢?比如,我们大部分的全局变量都是用锁保护的,那么是否还需要volatile呢?答案是否定的,即当全局变量由锁保护的时候,该全局变量是不需要volatile的。原因有二:一是锁保证了临界区的串行;二是锁的实现中有内存屏障,保证临界区访问全局变量为最新值。那么,就可以总结出,当没有锁保护的全局变量是需要使用volatile修饰的,如以下这种情况:
1. 全局变量int exit_flag = 0;
2. 线程1的主循环的退出条件是检查exit_flag是否为1,为1,则退出主循环;
3. 线程2在某些情况下,修改exit_flag为1。
另外如果是异步情况,即没有线程2,而有一个信号处理函数,在收到指定信号的情况下将exit_flag赋值为1。
这时,exit_flag都需要使用volatile来修饰。不然对于线程1的代码,如果编译器发现在线程1的代码中没有任何地方修改exit_flag,有可能会将exit_flag放入寄存器缓存。这样,在每次的条件检查的时候,都是从寄存器中读取,而非exit_flag对应的内存。这样将导致每次读取的值都为0,从而导致thread1无法退出。而使用volatile修饰exit_flag,会避免编译器做这种优化,强制每次读取都是从内存中读取,这样就可以保证了在exit_flag置为1时,thread1可以读取到最新值而退出。
 
那么现在有这样的一个问题。当有一个全局变量volatile int counter = 0作为一个计数器,而且同时有两个线程会修改这个计数器,这时这个counter是否需要锁的保护呢?
比如下面的代码:
  1. static volatile int counter = 0;
  2. void add_counter(void)
  3. {
  4. ++counter;
  5. }

在多线程的环境下,是否安全呢?counter的递增是否为我们所期待的结果呢?请大家先想一分钟!

 
为了得出答案,让我们看汇编代码:

  1. add_counter:
  2. pushl %ebp
  3. movl %esp, %ebp
  4. movl counter, %eax
  5. addl $1, %eax
  6. movl %eax, counter
  7. popl %ebp
  8. ret

看红色的这三行。怎么回事?为什么还是先把counter存入了寄存器eax,然后对eax进行操作,再将eax存入counter。这样的话,++counter就绝不会是原子性操作!必须由锁保护!

 
下面看看为什么汇编是这样的行为。究竟volatile提供了什么样的服务?!
首先修改一下之前的C代码:

  1. static int counter = 0;
  2. void add_counter(void)
  3. {
  4. for (; counter != 0x10; ++counter) {
  5. ++counter;
  6. }
  7. }

然后看其汇编代码,记得要是使用-O优化啊。

[fgao@fgao-vm-fc13 test]$ gcc -S -O test.c

  1. add_counter:
  2. pushl %ebp
  3. movl %esp, %ebp
  4. movl counter, %eax
  5. cmpl $16, %eax
  6. je .L4
  7. .L5:
  8. addl $2, %eax
  9. cmpl $16, %eax
  10. jne .L5
  11. movl %eax, counter
  12. .L4:
  13. popl %ebp
  14. ret

从上面的汇编代码中,可以很清楚的看出:这时将counter的值存入eax,然后每次给eax加2,然后使用eax与16比较,来完成C代码中的for循环的工作。

 
下面给counter加上volatile修饰符

  1. static volatile int counter = 0;
  2. void add_counter(void)
  3. {
  4. for (; counter != 0x10; ++counter) {
  5. ++counter;
  6. }
  7. }

依然打开编译器的优化开关

[fgao@fgao-vm-fc13 test]$ gcc -S -O test.c

  1. add_counter:
  2. pushl %ebp
  3. movl %esp, %ebp
  4. movl counter, %eax
  5. cmpl $16, %eax
  6. je .L4
  7. .L5:
  8. movl counter, %eax
  9. addl $1, %eax
  10. movl %eax, counter
  11. movl counter, %eax
  12. addl $1, %eax
  13. movl %eax, counter
  14. movl counter, %eax
  15. cmpl $16, %eax
  16. jne .L5
  17. .L4:
  18. popl %ebp
  19. ret

L5为对应C代码中的for循环的汇编实现。通过对比前一个汇编,我们很容易就发现了区别。虽然在执行counter递增的时候,两者都使用了寄存器eax。但是在每次访问counter的时候,前者会直接使用寄存器eax,而后者(使用volatile的时候)会重新从counter中读取最新值至eax,然后再使用eax。

 
OK。现在volatile的情况已经明了。volatile只提供了保证访问该变量时,每次都是从内存中读取最新值,并不会使用寄存器缓存该值——每次都会从内存中读取。而对该变量的修改,volatile并不提供原子性的保证。那么编译器究竟是直接修改内存的值,还是使用寄存器修改都符合volatile的定义。所以,一句话,volatile并不提供原子性的保证。

编写安全代码:小心volatile的原子性误解的更多相关文章

  1. 最新的JavaScript核心语言标准——ES6,彻底改变你编写JS代码的方式!【转载+整理】

    原文地址 本文内容 ECMAScript 发生了什么变化? 新标准 版本号6 兑现承诺 迭代器和for-of循环 生成器 Generators 模板字符串 不定参数和默认参数 解构 Destructu ...

  2. 最新的JavaScript核心语言标准——ES6,彻底改变你编写JS代码的方式!

    原文地址 迁移到:http://www.bdata-cap.com/newsinfo/1741515.html 本文内容 ECMAScript 发生了什么变化? 新标准 版本号6 兑现承诺 迭代器和f ...

  3. Java并发编程的艺术(二)——volatile、原子性

    什么是volatile Java语言允许线程访问共享变量,为了确保共享变量能够被准确一致地更新,如果一个字段被声明为volatile,那么Java内存模型将会确保所有线程看到这个变量时值是一致的.保证 ...

  4. [转]通过Visual Studio为Linux编写C++代码

    Build 2016大会上Microsoft首次公布的Visual Studio 2015扩展提供了在VS2015中编写C++代码,随后通过Linux/UNIX计算机进行编译和执行的能力.这种想法非常 ...

  5. 基于CkEditor实现.net在线开发之路(2)编写C#代码,怎么调用它。

    上一章简约的介绍了CkEditor编辑器,可以编辑js逻辑代码,css,html,C#代码,这章我根据实际例子,讲解怎么编写C#代码和怎么调用它. 大家都还记得刚刚接触程序编时的hello Word吧 ...

  6. 解决VS2012编写JQuery代码不能智能提示的问题(其他js库的代码提示设置估计类似)

    VS默认设置下编写jQuery代码是这样的: 解决办法: 1.在项目的"管理NuGet程序包"中安装JQuery: 2.打开:工具 -> 选项 -> 文本编辑器 -&g ...

  7. JNI技术基础(2)——从零开始编写JNI代码

    书接上文: <JNI技术基础(1)——从零开始编写JNI代码> 2.编译源程序HelloWorld.java并生成HelloWorld.class 3.生成头文件HelloWorld.h ...

  8. AppleWatch开发教程之Watch应用对象新增内容介绍以及编写运行代码

    AppleWatch开发教程之Watch应用对象新增内容介绍以及编写运行代码 添加Watch应用对象时新增内容介绍 Watch应用对象添加到创建的项目中后,会包含两个部分:Watch App 和 Wa ...

  9. mvn编写主代码与测试代码

    maven编写主代码与测试代码 3.2 编写主代码 项目主代码和测试代码不同,项目的主代码会被打包到最终的构件中(比如jar),而测试代码只在运行测试时用到,不会被打包.默认情况下,Maven假设项目 ...

随机推荐

  1. XSS与CSRF两种跨站攻击比较

    XSS:跨站脚本(Cross-site scripting) CSRF:跨站请求伪造(Cross-site request forgery) 在那个年代,大家一般用拼接字符串的方式来构造动态SQL 语 ...

  2. oracle中int类型和number类型区别

    INT类型是NUMBER类型的子类型.下面简要说明:(1)NUMBER(P,S)该数据类型用于定义数字类型的数据,其中P表示数字的总位数(最大字节个数),而S则表示小数点后面的位数.假设定义SAL列为 ...

  3. 宜昌IT软件资源汇总

    三峡云计算中心大厦 http://castd-ycci.com/ 三峡云计算中心http://www.sanxiacloud.com/index.htm 宜昌创业者(罗新:小卡片做成大产业 年销售额5 ...

  4. objective-c 在线视频 学习资料...

    ---视频 http://www.lanou3g.com/newslist.php?cid=7 http://edu.51cto.com/lesson/id-15489.html http://www ...

  5. Linux下Keepalived 安装与配置

    Keepalived 安装与配置 一.环境说明 1.操作系统内核版本:2.6.9-78.ELsmp 2.Keepalived软件版本:keepalived-1.1.20.tar.gz 二.环境配置 1 ...

  6. HDU1312——Red and Black(DFS)

    Red and Black Problem DescriptionThere is a rectangular room, covered with square tiles. Each tile i ...

  7. Android TextView和EditText属性详解

    TextView属性详解: autoLink设置 是否当文本为URL链接/email/电话号码/map时,文本显示为可点击的链接.可选值(none/web /email/phone/map/all) ...

  8. Shipping Transactions > Error: The action can not be performed because the selected records could not be locked.

    Shipping Transactions > Action: Launch Pick Release (B: Go) Error: The action can not be performe ...

  9. 关于SqlParameter中IN子句查询的问题

    今天调试到方法中代码: String hotelCodes =”’’,’’,’’”; string sqltext ="select * from HotelMedalInfo where ...

  10. apache开源项目--Apache Drill

    为了帮助企业用户寻找更为有效.加快Hadoop数据查询的方法,Apache 软件基金会发起了一项名为“Drill”的开源项目.Apache Drill 实现了 Google's Dremel. Apa ...