1.5 原子性、内存可见性和重排序——重新认识synchronized和volatile
对于涉及共享变量(Shared Variable)访问的操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作(Atomic Operation),相应地我们称该操作具有原子性(Atomicity)。例如,对int型共享变量counter执行counter++操作就不是原子操作,这是因为counter++实际上可以分解为3个子操作:
• 将变量counter的当前值加载(读取)到寄存器R中。
• 将寄存器R的值增加1。
• 将寄存器R的值写入变量counter。
在多线程环境中,非原子操作可能会受其他线程的干扰。比如,上述例子如果没有对相应的代码进行同步(Synchronization)处理,则在执行第2个子操作的时候可能出现counter值已经被其他线程修改的情况,此时这一步的操作所使用的counter变量的“当前值”其实是过期的。当然,synchronized关键字可以帮助我们实现操作的原子性,以避免这种线程间干扰的情况。
synchronized关键字可以实现操作的原子性,其本质是通过该关键字所限定的临界区(Critical Section)的排他性来保证在任一时刻只有一个线程能够执行临界区中的代码,这使得临界区中的代码代表了一个原子操作。关于这一点,读者可能已经很清楚。但是,synchronized关键字所起的另一个作用——保障内存可见性(Memory Visibility),也值得我们回顾。
CPU在执行代码的时候,为了降低变量访问的时间开销,可能将代码中访问的变量值缓存到该CPU的高速缓存(如L1 Cache、L2 Cache等)中。因此当相应代码再次访问某个变量时,相应的值可能是从CPU的高速缓存而不是主内存中读取的。同样地,出于对内存访问效率的考虑,代码对变量值的修改也可能仅被写入执行这段代码的CPU上的写缓冲器(Store Buffer)里,而没有被写入该CPU的高速缓存里,更没有被写入主内存里。由于每个CPU都有自己的高速缓存,而一个CPU并不能直接读取其他CPU上的高速缓存里的内容,这就导致一个线程对共享变量所做的更新可能无法被其他CPU上运行的其他线程“看到”。这就是所谓的内存可见性。
synchronized关键字的另一个作用就是,它保证了一个线程执行临界区中的代码时所修改的变量值对于稍后执行该临界区中的代码的线程来说是可见的。这对于保证多线程代码的正确性来说非常重要。
而volatile关键字也能够保证内存可见性,即一个线程对一个采用volatile关键字修饰的变量的值的更改对于其他访问该变量的线程而言总是可见的。也就是说,其他线程不会读到一个“过期”的变量值。因此,有人将volatile关键字与synchronized关键字所代表的内部锁做了比较,将其称为轻量级的锁。这种称呼其实并不恰当,volatile关键字只能保证内存可见性,它并不能像synchronized关键字所代表的内部锁那样保证操作的原子性。volatile关键字保障内存可见性的核心机制是,当一个线程修改了一个volatile关键字修饰的变量的值时,该值会被写入当前线程所在的CPU上的高速缓存里,而不是仅仅停留在该CPU的写缓冲器里,而其他CPU上的高速缓存里存储的该变量的值(副本)也会因此而失效。这就保证了这些其他线程再访问该volatile关键字修饰的变量时总是可以通过处理器的缓存一致性协议(Coherence Protocol)来获取该变量的最新值。
volatile关键字的另一个作用是禁止了指令重排序(Re-order)[10]。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式(源代码所指定的顺序)进行的。例如,下面的实例变量初始化语句
所做的事情非常简单:
1)创建类SomeClass的实例。
2)初始化SomeClass实例。
3)将类SomeClass实例的引用赋值给变量someObject。
但是由于指令重排序的作用,这段代码的实际执行顺序可能是:
1)创建SomeClass类的实例。
2)将对SomeClass实例的引用赋值给变量someObject。
3)初始化SomeClass实例(即执行SomeClass类的构造器)。
因此,当其他线程访问someObject变量的值时,其得到的仅是指向一段存储SomeClass实例的内存空间的引用而已,而该内存空间相应的SomeClass实例的初始化可能尚未完成,这就可能导致一些意想不到的结果。而禁止指令重排序则可以使上述代码按照我们所期望的顺序(正如代码所表达的顺序)来执行。
禁止指令重排序虽然导致编译器和CPU无法对一些指令进行可能的优化,但是它在某种程度上让代码的执行看起来更符合我们的期望。
本书涉及的代码也有不少地方使用了volatile关键字。读者需要注意这个关键字对多线程代码的正确性所起的作用。
与volatile相比,synchronized既能保证操作的原子性,又能保证内存可见性,而volatile仅能保证内存可见性。但是,synchronized会导致上下文切换,而synchronized不会。