您的位置:首页 > 其它

JVM原子操作的实现与一点改进想法

2013-04-13 10:39 183 查看
"原子操作(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不用后者的方式来实现原子++,或许是解释执行的时候技术所限,这点需要我们有时间去继续挖掘了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: