要深刻理解volatile这个关键字的用法及作用,需要补充以下知识:
1、 内存访问操作/指令执行操作的乱序:假设每个CPU都分别运行着一个会触发内存访问操作的程序。那么对于这样一个CPU,其内存访问顺序是非常松散的,在保证程序上下文逻辑关系的前提下,CPU可能乱序执行内存操作。此外,编译器也可以将它输出的指令安排成任何它喜欢的顺序,只要保证不影响程序表面的执行逻辑。这里就涉及到了两次可能发生指令重排的情况:一个是编译的时候,由编译原理的知识知道,编译器会对代码进行优化,这一步就涉及到指令重排,当然,编译完成之后的目标代码中指令的顺序就是确定的,不同线程执行该代码的顺序是一样的;另一个就是CPU在执行具体的指令的时候,也会因为计算机当前的状态(比如寄存器的占用情况、ALU的使用情况,cup缓存层的存在等原因)的不同导致指令最终的执行顺序发生变化(实际上,cpu本身并不会对指令进行重排,它本身是按照编译后的顺序来执行指令的,只是由于执行不同的指令需要的时间长短不同,以及缓存层的存在,再加上CPU执行指令的流水线并不是串行化等因素,那么就有可能出现排在靠前位置的指令还没执行完,而排在靠后的指令已经执行完了的情况,这一情况就是所谓的CPU执行指令的乱序,具体原因后面会更详细地解释),尽管这个变化可能不影响最终结果的正确性。
2、 CPU指令执行乱序的原因:现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取址,译码,访存,执行,写回等若干个阶段。又因为指令流水线并不是串行化的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”前的阶段上。相反,流水线中的多个指令是可以同时处于同一个阶段的,只要CPU内部相应的处理部件未被占满。比如说CPU只有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于“执行”阶段,而两条加法指令在“执行”阶段就只能串行工作。这样一来,乱序就可能产生了。比如一条加法指令出现在一条除法指令的后面,但由于除法的执行时间很长,在它执行完之前,加法可能就先执行完了。再比如两条访存指令,可能由于第二条指令中了cache(或其它原因)而导致它先于第一条指令完成。
3、 CPU执行指令乱序进一步说明:一般情况下,指令乱序并不是CPU在执行指令之前刻意去调整顺序,CPU总是顺序地去内存里取指令,然后将其顺序地放入指令流水线。但是指令执行时的各种条件,指令与指令之间的相互影响,可能导致顺序放入流水线的指令,最终乱序执行完成。这就是所谓的“顺序流入,乱序流出”。指令流水线除了在资源不足的情况下会卡住之外(如前所述的一个加法器应付两条加法指令),指令之间存在的相互依赖才是导致流水线阻塞的主要原因。当然,CPU的乱序执行并不是任意地乱序,而必须保证上下文依赖逻辑的正确性。比如:a++;b=f(a);由于b=f(a)这条指令依赖于第一条指令(a++)的执行结果,所以b=f(a);将在“执行”阶段之前被阻塞,直到a++的执行结果被生成出来。
另外一个CPU执行乱序的示例如下:
对于处理器A和处理器B都是按顺序分别执行A1和A2,以及B1和B2指令。以处理器A为例,为了提高执行效率,处理器执行A1只会将a=1写到缓冲区,紧接着就回执行A2指令,然后在适当的时候,才会将缓冲区中的数据回写主内存,即A3操作。所以尽管从处理器A的角度来看,执行顺序是A1->A2,但从内存操作实际发生的顺序来看确是A2->A1。因为A1操作只写了缓冲区,实际上直到处理器A执行完A3将缓冲区刷新到主存后,写操作A1才算真正执行完成。从这个角度也可以看出前面提到的,cpu不会刻意调整指令执行顺序,它本身是按照编译后的顺序来执行指令的,只是由于执行不同的指令需要的时间长短不同,以及缓存层的存在,再加上CPU执行指令的流水线并不是串行化的等因素导致cpu的最终执行效果是“顺序流入,乱序流出”。
4、 编译器指令重排(代码优化)的原理:如果两条有依赖关系(像刚刚列举的a++;b=f(a);)的指令挨得很近,后一条指令必定会因为等待前一条执行的结果,而在流水线中阻塞很久(这个“很久”是对计算机而言哈)。而编译器的乱序,作为编译优化的一种手段,则试图通过指令重排,在这两条指令之间插入其他指令,将这