JVM原子操作的实现与一点改进想法__无相_的博客-程序员宝宝

技术标签: JVM  Java  多核  处理器  原子  

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/pgj007/article/details/8796261

智能推荐

ubuntu18.04.1 开机默认进入命令行模式/用户图形界面_to boot into default mode_AllenLeungX的博客-程序员宝宝

一、开机默认进入命令行模式1、输入命令:sudo systemctl set-default multi-user.target 2、重启:reboot要进入图形界面,只需要输入命令startx从图形界面切换回命令行:ctrl+alt+F7二、开机默认进入图形用户界面1、输入命令:sudo systemctl set-default graphical.target 2、重启:rebo...

fatal error C1083: Cannot open include file: 'iostream.h': No such file or directory_dkyptnz538386的博客-程序员宝宝

有初学C++者经常会出现如下错误:fatal error C1083: Cannot open include file: 'iostream.h': No such file or directory。这个错误一般在使用Visual C++ .NET 2005时出现。原因,iostream.h为C类库,C++类库中应该为iostream。另外ci...

【我就看看不说话】UITextView NSTextContainer NSLayoutManager NSTextStorage_居然是村长的博客-程序员宝宝

先上一张图:这是使用UITextView时用到的iOS7新增加的类:NSTextContainer、NSLayoutManager、NSTextStorage及其相互关系:这三个新出的类还没有在官方出独立的class reference,但是在新出的UIKit_Framework上已经有这些类的相关说明及使用方法,当然官方还会更新。以下是

tf.compat_Wanderer001的博客-程序员宝宝

一、模块1、Modulesv1 module: 将所有的公共TensorFlow接口引入到这个模块中。1、v1 模块模块列表:app:通用入口点脚本。 audio:tf.audio命名空间的公共API。 autograph:将普通Python转换为TensorFlow图形代码。 bitwise:操作整数的二进制表示的操作。 compat:Python 2与Python ...

数论概论读书笔记 39.斐波那契与线性递归序列_Feynman1999的博客-程序员宝宝

斐波那契与线性递归序列比内公式 斐波那契序列FnFnF_n用递归公式描述如下: F1=F2=1,Fn=Fn−1+Fn−2,n=3,4,5,...F1=F2=1,Fn=Fn−1+Fn−2,n=3,4,5,...F_1=F_2=1,\quad F_n=F_{n-1}+F_{n-2}\quad ,n=3,4,5,... 则斐波那契序列的第nnn项可用公式 Fn=15–√{(1+5–√2)...

Java集合详细讲解_HuCheng1997的博客-程序员宝宝

Java集合:Set、List、Queue、Map

随便推点

APP自动化测试------环境搭建_旁边有只牛的博客-程序员宝宝

Android 自动化环境搭建**一.Node.js** Appium server 的运行环境(必备依赖条件)。二.Appium server 两种安装方式: 1)通过Nmp命令来安装。 命令:npm install -g appium 备注:安装完后都是纯命令行。 2)安装appimun desktop 版本 可从官网直接下载最新版本。三.JDK jdk1.8 64位及以上版本。四.Android sd

力扣刷题(python)50天——第四十七天:除自身以外数组的乘积_if 雨田人尹==雷伊:的博客-程序员宝宝

力扣刷题(python)50天——第四十七天:除自身以外数组的乘积题目描述给定长度为 n 的整数数组 nums,其中 n &gt; 1,返回输出数组 output ,其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。示例:输入: [1,2,3,4]输出: [24,12,8,6]说明: 请不要使用除法,且在 O(n) 时间复杂度内完成此题。进阶:你...

iOS imageName方法获取Folder文件夹(蓝色文件夹)内图片_diaoquan3287的博客-程序员宝宝

Xcode创建的iOS项目内存在两种文件夹:Group(黄色, 伪文件夹) 和Folder(蓝色, 真文件夹):Group:Folder:Images.xcassets或Group文件夹内的PNG图片可通过imageNamed方法直接加载:[UIImage imageNamed:@"photo"];Folder文件夹内的PNG图片通过imageName...

文件上传时,出现 java.lang.ClassNotFoundException,可能时jar有冲突_立志当大佬的博客-程序员宝宝

&lt;!-- 传统方式文件上传--&gt; &lt;dependency&gt; &lt;groupId&gt;commons-fileupload&lt;/groupId&gt; &lt;artifactId&gt;commons-fileupload&lt;/artifactId&gt; &lt;version&gt;1.2.1&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &

abcde / fghij = n_按从小到大的顺序输出所有形如abcde/fghij=n的表达式,每个表达式占据1行。_-x_x-的博客-程序员宝宝

问题:输入正整数n,按从小到大的顺序输出所有形如abcde/fghij=n的表达式,其中a~j恰好为数字0~9的一个排列,2&amp;lt;=n&amp;lt;=79。样例输入: 62样例输出: 79546/01238=62                   94736/01528=62解:public class Main { static int[] a = new int[10]; public ...

Aurora8B10B IP使用 -05- 收发测试应用示例_aurora ip发送端和接收端_Vuko-wxh的博客-程序员宝宝

本文首先介绍了根据网上博文简单介绍了8B/10B的原理,并根据Aurora IP的相关使用方法,参考IP手册进行设计递增数测试用例,并下板进行实际验证。

推荐文章

热门文章

相关标签