本文的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. Recommender Systems引言

    我想站在大神肩膀上...貌似是计算所的一个小伙伴... 总结的很好,看得出来有一定的功底.... 不过对于自己看过了的东西,就不愿意再翻看第二遍了.恰好这本书和项亮那本很像,就直接看这本书了.顺便记记 ...

  2. Binary search for the first element greater than target

    We all know how to search through an array for an element whose value equals the target value, but h ...

  3. [转载]c# winform 获取当前程序运行根目录

    // 获取程序的基目录. System.AppDomain.CurrentDomain.BaseDirectory // 获取模块的完整路径. System.Diagnostics.Process.G ...

  4. ural 1200

    推出公式  然后特判两端  代码其实挺烂      但是有人竟然可以直接暴过去的 ...... #include <cstdio> #include <cstring> #in ...

  5. hdu 2897 邂逅明下 博弈论

    这个分区间: 1<=n%(p+q)<=p 必败 其他必胜! 代码如下: #include<iostream> #include<stdio.h> #include& ...

  6. cocos2d-x 多分辨率适配详解(转载),以前北京团队设计的游戏,也是用这套方案

    http://blog.csdn.net/kyo7552/article/details/17163487 多种分辨率的适配一直都是一个蛋疼的问题,各家公司可能都有自己的一套方案.今天我为大家介绍的是 ...

  7. Python之函数篇

    函数形参 函数取得的参数是你提供给函数的值,这样函数就可以利用这些值 做 一些事情.这些参数就像变量一样,只不过它们的值是在我们调用函数的时候定义的,而非在函数本身内赋值. 参数在函数定义的圆括号对内 ...

  8. Spring Framework 4.1.1

    http://repo.spring.io/libs-release-local/org/springframework/spring/4.1.1.RELEASE/

  9. 当当开源sharding-jdbc,轻量级数据库分库分表中间件

    近期,当当开源了数据库分库分表中间件sharding-jdbc. Sharding-JDBC是当当应用框架ddframe中,从关系型数据库模块dd-rdb中分离出来的数据库水平分片框架,实现透明化数据 ...

  10. 转载:C++ list 类学习笔记

    声明:本文转自http://blog.csdn.net/whz_zb/article/details/6831817 双向循环链表list list是双向循环链表,,每一个元素都知道前面一个元素和后面 ...