引言

现在,我们先将目光从共享数据、临界区和互斥这些概念上移开,来看一下在最底层的硬件系统中,一个原子操作是怎么实现的。你目前还没有任何关于实现互斥的知识,这样正好,你不必考虑操作系统和应用程序,只需要考虑硬件就行。
本文的大部分内容参考的是《intel指令参考手册》,所以之后的描述只适用于intel系列的硬件。

一个简单的机器指令

所有的高级语言最终都是以机器指令的形式在硬件cpu上执行,我们考虑下面这个简单的一元操作指令:

  1. INC D

INC指令用来对D指定的数据加1,D既可以是一个寄存器,也可以是一个内存位置,当它代表一个内存位置的时候,CPU需要怎么做呢?首先,它需要把该数据从内存中读取到cpu的寄存器中,然后对寄存器中的数据加1,最后把得到的结果写回到那个内存位置上。注意,这三个步骤虽然逻辑上是连续的,但整体来说肯定不是原子的,这其实是一个典型的读-修改-写的操作,更通俗的解释是,在多cpu机器上,当一个cpu执行这个操作的时候,另外一个cpu也可能在访问这个内存位置并对它进行修改,这样,在机器指令层面上,也会出现竞态条件。
如果你明白了上面的解释,那么java中的count++操作为什么会出现数据竞争,你肯定就很清楚了。
类似这样涉及到内存的读-修改-写操作的机器指令还有很多,这里不再罗列。
那既然机器指令同样面临这个问题,硬件是怎么解决的呢?下面,我根据《intel指令参考手册》中的内容来解释一下(很多都是基于个人理解的翻译,不一定准确)。

原子操作的实现

原子操作的三种机制

Intel处理器支持对系统内存(system memory)的锁定原子操作(locked atomic operations)。这些操作被用来管理共享的数据结构,这些共享数据可能会被多个处理器同时访问。处理器主要使用下面三个相互依赖的机制来提供锁定原子操作:

  • 基本的原子操作。(Guaranteed atomic operations)。
  • 总线锁,使用LOCK#信号和LOCK指令前缀。(Bus locking, using the LOCK# signal and the LOCK instruction prefix)。
  • 缓存一致性协议保证原子操作可以在缓存的数据结构上执行(缓存锁定);这种机制存在于奔腾4、英特尔至强和P6系列处理器中。(Cache coherency protocols that ensure that atomic operations can be carried out on cached data structures (cache lock); this mechanism is present in the Pentium 4, Intel Xeon, and P6 family processors)。

下面,我先大致介绍一下这三种机制的含义和关系:
这三个机制是相互依赖的。对于第一个机制,特定的基本内存事务(例如从系统内存中读取或者写入一个字节)会以原子方式进行处理,也就是说,这类操作一旦开始,处理器保证这个操作会在其他处理器被允许访问这个内存位置之前完成,注意, 这里说的从系统内存中读取或者写入一个字节(byte)并不是一条指令,而是类似上面我们说的INC这条指令的从内存中读取数据和将结果写回内存这两步,所以上面我们说INC D当D是内存位置时这个指令不是原子操作是没有问题的。
对于不能由处理器保证的内存操作,例如类似INC这类读-修改-写操作,处理器支持总线锁来实现原子性。
因为经常访问的内存会被缓存在cpu的一级或者二级缓存里,所以原子操作可以在处理器的缓存中进行而不需要使用总线锁,在这种情况下,处理器的一致性协议保证其他缓存了相同内存位置的处理器在原子操作发生时会被正确的处理。
由于篇幅限制,我们只看一下基本的原子操作(guarantedd atomic operations)。关于总线锁和缓存锁,我们放在下篇文章介绍。

基本原子操作(Guaranteed Atomic Operations)

Intel486处理器(以及该系列更新的处理器)保证下面的基础内存访问总是原子的:

  • 读或者写一个字节。
  • 对一个字长(对齐到16位边界)的读写。
  • 对两个字长(对齐到32位边界)的读写。

Pentium处理器(以及该系列更新的处理器)额外保证下面的内存操作总是原子的:

  • 对四个字长(对齐到64位边界)的读写。
  • 在32位总线上对未缓存的16位内存位置的访问。

P6系列处理器(以及该系列更新的处理器)额外保证下面的内存操作总是原子的:

  • 在一个缓存行上对缓存的16位、32位和64位的访问。

我这里给出计算机中字的概念:每台计算机都有一个字长,指明指针数据的标称大小。因为虚拟内存是以这样一个字来编码的,所以字长决定的最终要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为w为的机器而言,虚拟地址的范围是0到2的w次方-1,程序最多访问2的w次方字节。常见的字长就是32位和64位。
如果可缓存的数据跨缓存行或者页边界,那么在Intel Core 2 Duo, Intel® AtomTM, Intel Core Duo, Pentium M, Pentium 4, Intel Xeon, P6 family, Pentium, and Intel486 这些处理器上就不能保证是原子的。
关于Guaranteed Atomic Operations,《Intel指令参考手册》上还有一些内容,这里不再罗列。我们知道硬件保证了这些操作的原子性即可,再强调一遍,这里的读和写并不代表INC这类的指令,而只是指令中的一步,也就是说Guaranteed Atomic Operations这个机制并没有保证类似INC D这类读-修改-写机器指令的原子性,它需要总线锁或者缓存锁来保证。

小结

Guraranteed Atomic Operations保证了基本内存操作的原子性,但是它不能实现类似INC这类读-修改-写指令的原子性,下篇文章,我们来看总线锁和缓存锁定,通过这两个机制,我们就能实现读-修改-写指令的原子性。