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

来源:互联网 发布:c语言书籍下载txt 编辑:程序博客网 时间:2024/06/06 01:08

"原子操作(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不用后者的方式来实现原子++,或许是解释执行的时候技术所限,这点需要我们有时间去继续挖掘了。