"原子操作(atomic operation)是不需要synchronized",这是Java多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断。
在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。但是,在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。
在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
那么原子操作到底用在哪里呢,下面先从一个简单的JAVA例子入手:
3 import java.util.concurrent.atomic.*;
4
5 public class VolatileTest
6 {
7 public static volatile int race = 0;
8 final static AtomicInteger value = new AtomicInteger(0);
9 public static void increase()
10 {
11 value.incrementAndGet();
12 race++;
13 }
14 private static final int THREADS_COUNT = 20;
15
16 public static void main(String[] args)
17 {
18 Thread[] threads = new Thread[THREADS_COUNT];
19 for (int i=0; i<THREADS_COUNT; i++)
20 {
21 threads[i] = new Thread(new Runnable() {
22 @Override
23 public void run() {
24 for (int i=0; i<10000; i++)
25 {
26 increase();
27 }
28 }
29 });
30 threads[i].start();
31 }
32
33 while (Thread.activeCount() > 1)
34 Thread.yield();
35
36 System.out.println(race);
37 System.out.println(value.get());
38 }
39 }
上面例子的执行结果是:
195333
200000
不过多运行几次大家就会发现race的值并不固定,因为increase方法没有加synchronized,但是value的值每次都能保证正确,这是因为value用到了原子操作。到这里大家又要问了,既然synchronized可以保障同步,那么要原子操作干啥,这个是处于效率考虑,大量的线程切换会带来效率上的损失,或许jvm对于synchronized做了一些优化,但是也很难达到原子的效率,再者,为了一个变量的++就考虑使用全方法甚至全对象synchronized是不是太奢侈了。
下面我们来剖析一下incrementAndGet的实现,incrementAndGet->compareAndSet->compareAndSwapInt->Unsafe_CompareAndSwapInt ->Atomic::cmpxchg
public final int incrementAndGet() {
for (;;) {
int current = get(); // 获得value当前值
int next = current + 1;
if (compareAndSet(current, next)) // 此方法比较current跟此时对象value值,如果相等,将next的值赋值给value;如果不相等,说明正在有人改变value,则返回FALSE,循环下次继续
return next;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//比较成功,此函数即返回TRUE
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value // eax一般存放函数返回值,即compare_value为返回值
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx // [edx]与eax如果相等,ecx->[edx];否则[edx]->eax
}
}
以上是几个主要函数,前面两个JAVA代码方法应该不是难懂,compareAndSwapInt到 Unsafe_CompareAndSwapInt 已经不是直接的调用关系了,这一步是JVM的解释器进行的,也就是说Unsafe_CompareAndSwapInt以下就不是java的代码了。两个c++、汇编实现的函数也不是太难理解,其实在X86下就是使用cmpxchg指令实现CAS(比较并交换),不过LOCK_IF_MP这个宏用的还是很精炼的,为了让大家加深Atomic的理解,我们再看一下Atomic中inc(实现原子的++操作)的代码,这个相对简单些:
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \ // mp与0比较
__asm je L0 \ // mp为0则跳到L0标记
__asm _emit 0xF0 \ // _emit伪指令,作用嵌入0xF0到当前代码处,0xF0其实就是lock指令的机器码
__asm L0:
inline void Atomic::inc (volatile jint* dest)
{
// alternative for InterlockedIncrement
int mp = os::is_MP(); // 判断是否为对称多处理器(0为单核,1为多核)
__asm // C/C++中嵌入汇编
{
mov edx, dest; // 地址赋值给edx寄存器
LOCK_IF_MP(mp) // 宏(用法类似函数),详见上面注释
add dword ptr [edx], 1; // [edx]加1
}
}
有了上面的注释,这段代码应该不难理解,效果等价于以下伪代码:
If (单核)
{
__asm // C/C++中嵌入汇编
{
mov edx, dest; // 地址赋值给edx寄存器
add dword ptr [edx], 1; // [edx]加1
}
}
else
{
__asm // C/C++中嵌入汇编
{
mov edx, dest; // 地址赋值给edx寄存器
lock add dword ptr [edx], 1; // [edx]加1
}
}
或许90%的以上的汇编程序员能写出此伪码,但是只有不到10%的程序员会想到用_emit 嵌入指令,很不幸的是,我也在这90%之中,万幸的是,当我第一次看到此代码,立刻就意识到0xF0是lock的机器码。
除了直接提供给java使用之外,Atomic还充斥着jvm的几乎任何其他地方,只是我们平时没注意罢了,可以这样认为,我们的java代码运行时无时无刻都有大量Atomic的方法被调用,也即无时无刻都有大量LOCK_IF_MP被调用(宏毕竟不是函数,这里讲调用不是很恰当,知其意即可),这样就带来了这样一个思索,LOCK_IF_MP这个宏的确写的不错,但是这或许不是一个最恰当的方法,个人认为,在我们java程序运行的时候一般不会发生CPU核心数变化的情况,完全可以使用预编译方式来代替这个无时无刻都要进行的判断,换句话说,单核上使用单核原子版本,多核上使用多核原子版本,在我们安装jdk的时候判断CPU核心数然后安装相应版本就可以了。
后记:如果大家仔细看到这里,会发现一个问题,java中所谓的原子++实际上使用的是CAS,即类似自旋锁之类的机制,而C++中实现原子++并不依赖CAS,很显然,后者效率更高一些,至于为何java中AtomicInteger不用后者的方式来实现原子++,或许是解释执行的时候技术所限,这点需要我们有时间去继续挖掘了。