作者
王璞
垃圾回收机制(GC)对大部分开发者来说应该不陌生,特别是Java开发者或多或少都跟GC打过交道。GC的优点是实现对堆上分配的内存动态回收,避免内存泄漏。但是GC的缺点是对性能有一定影响,特别是stoptheworld问题,而且GC什么时候回收内存是不确定的,开发者无法知晓。
在无锁化编程场景下,用Java这种有GC的语言,一定程度简化了对内存的管理,降低了无锁化编程的难度。无锁化编程,顾名思义,就是不用锁。无锁化编程特指在多线程编程的时候,对线程间共享数据的并发修改不使用锁,而采用基于硬件提供的原子操作能力来修改共享数据,进而提升性能,减少使用锁带来的互斥开销。
最常用的硬件提供的原子操作是CompareandSwap(简称CAS),编程语言基于硬件提供的原子操作能力封装出CAS库函数调用。先看下C++里的CAS函数调用:
这两个函数的功能一样,只是返回值不同。为了简化描述,略去了一些细节(这个细节是关于内存顺序,即程序指令在执行时的顺序问题,这个问题是无锁化编程能否正确实现的关键问题之一,后面我再专门详细介绍)。第一个CAS函数比较指针ptr指向的变量,看是不是等于oldval,如果相等就把ptr指向的变量改为newval,并返回true,否则不做任何改变并返回false。第二个CAS函数也是比较ptr指向的变量是否等于oldval,如果相等就把该变量改为newval,并返回oldval,如果不等就不做改变,并返回ptr指向变量的当前值。C++的CAS函数调用可以保证对ptr指向的变量的修改是原子的,要么更改完成,要么不做更改。
再看下硬件提供的原子操作。比如X86架构提供了一条CAS指令cmpxchg,这条指令在执行时是由硬件保证原子性,不会被打断。其他硬件架构,比如ARM、PowerPC也提供类似的CAS原子操作,但是和X86的实现机制不一样,这里先不展开。编程语言的编译器在X86架构下编译时,会负责把CAS库函数调用编译成基于cmpxchg的机器代码。比如C++的编译器GCC在编译时如果碰到上面两个CAS函数调用,会生成包含cmpxchg指令的目标码。
无锁化编程示例:无锁化堆栈(Lock-FreeStack)的Java实现
先来看个简单的无锁化编程的例子,一个无锁化堆栈的Java实现(从网上找了一段现成的代码,没经过编译验证,仅做示例):
从代码可以看出,这个栈的实现完全没有用锁,栈顶top是当前堆栈顶端节点的原子引用AtomicReference,每次出栈(pop)入栈(push)的时候,调用AtomicReference的