关于java:并发CAS机制你真的理解了嘛深入到操作系统分析

38次阅读

共计 4869 个字符,预计需要花费 13 分钟才能阅读完成。

学习 Java 并发编程,CAS 机制都是一个不得不把握的知识点。这篇文章次要是从呈现的起因再到原理进行一个解析。心愿对你有所帮忙。

一、为什么须要 CAS 机制?

为什么须要 CAS 机制呢?咱们先从一个谬误景象谈起。咱们常常应用 volatile 关键字润饰某一个变量,表明这个变量是全局共享的一个变量,同时具备了可见性和有序性。然而却没有原子性。比如说一个常见的操作 a ++。这个操作其实能够细分成三个步骤:

(1)从内存中读取 a

(2)对 a 进行加 1 操作

(3)将 a 的值从新写入内存中

在单线程状态下这个操作没有一点问题,然而在多线程中就会呈现各种各样的问题了。因为可能一个线程对 a 进行了加 1 操作,还没来得及写入内存,其余的线程就读取了旧值。造成了线程的不平安景象。如何去解决这个问题呢?最常见的形式就是应用AtomicInteger 来润饰a。咱们能够看一下代码:

 // 应用 AtomicInteger 定义 a
 static AtomicInteger a = new AtomicInteger();
 public static void main(String[] args) {Test3 test = new Test3();
  Thread[] threads = new Thread[5];
  for (int i = 0; i < 5; i++) {threads[i] = new Thread(() -> {
    try {for (int j = 0; j < 10; j++) {
      // 应用 getAndIncrement 函数进行自增操作
      System.out.println(a.incrementAndGet());  
      Thread.sleep(500);
     }
    } catch (Exception e) {e.printStackTrace();
    }
   });
   threads[i].start();}
 }
}

当初咱们应用 AtomicInteger 类并且调用了 incrementAndGet 办法来对 a 进行自增操作。这个 incrementAndGet 是如何实现的呢?咱们能够看一下 AtomicInteger 的源码。

* Atomically increments by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

咱们到这一步能够看到其实就是 usafe 调用了 getAndAddInt 的办法实现的,然而当初咱们还看不出什么,咱们再深刻到源码中看看 getAndAddInt 办法又是如何实现的,

 int var5;     
 do {var5 = this.getIntVolatile(var1, var2);   
 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));    
 return var5;   
}

到了这一步就略微有点头绪了,原来底层调用的是 compareAndSwapInt 办法,这个 compareAndSwapInt 办法其实就是 CAS 机制。因而如果咱们想搞清楚 AtomicInteger 的原子操作是如何实现的,咱们就必须要把 CAS 机制搞清楚,这也是为什么咱们须要把握 CAS 机制的起因。

二、剖析 CAS

1、根本含意

CAS 全拼又叫做compareAndSwap,从名字上的意思就晓得是比拟替换的意思。比拟替换什么呢?

过程是这样:它蕴含 3 个参数 CAS(V,E,N),V 示意要更新变量的值,E 示意预期值,N 示意新值。仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则阐明曾经有其余线程做两个更新,则以后线程则什么都不做。最初,CAS 返回以后 V 的实在值。

咱们举一个我之前举过的例子来阐明这个过程:

比如说给你儿子订婚。你儿子就是内存地位,你本来认为你儿子是和杨贵妃在一起了,后果在订婚的时候发现儿子身边是西施。这时候该怎么办呢?你一气之下不做任何操作。如果儿子身边是你料想的杨贵妃,你一看很开心就给他们订婚了,也叫作执行操作。当初你应该明确了吧。

CAS 操作时抱着乐观的态度进行的,它总是认为本人能够胜利实现操作。所以 CAS 也叫作乐观锁,那什么是乐观锁呢?乐观锁就是咱们之前赫赫有名的synchronized。乐观锁的思维你能够这样了解,一个线程想要去取得这个锁然而却获取不到,必须要他人开释了才能够。

2、底层原理

想要弄清楚其底层原理,深刻到源码是最好的形式,在下面咱们曾经通过源码看到了其实就是 Usafe 的办法来实现的,在这个办法中应用了 compareAndSwapInt 这个 CAS 机制。因而,当初咱们有必要进一步深刻进去看看:

    // compareAndSwapInt 是 native 类型的办法
    public final native boolean compareAndSwapInt(
     Object o, 
     long offset,
        int expected,
        int x
    );
    // 残余还有很多办法
}

咱们能够看到这外面次要有四个参数,第一个参数就是咱们操作的对象 a,第二个参数是对象 a 的地址偏移量,第三个参数示意咱们期待这个 a 是什么值,第四个参数示意的是 a 的理论值。

不过这里咱们会发现这个 compareAndSwapInt 是一个 native 办法,也就是说再往下走就是 C 语言代码,如果咱们放弃好奇心,能够持续深刻进去看看。

           jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  // 依据偏移量 valueOffset,计算 value 的地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // 调用 Atomic 中的函数 cmpxchg 来进行比拟替换
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

下面的代码咱们解读一下:首先应用 jint 计算了 value 的地址,而后依据这个地址,应用了 Atomiccmpxchg办法进行比拟替换。当初问题又抛给了这个cmpxchg,实在实现的是这个函数。咱们再进一步深刻看看,假相曾经离咱们不远了。

                         volatile unsigned int* dest, 
                         unsigned int compare_value) {assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
  /*
   * 依据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载函数
  */
    return (unsigned int)Atomic::cmpxchg((jint)exchange_value, 
                     (volatile jint*)dest, (jint)compare_value);
}

皮球又一次被完满的踢走了,当初在不同的操作系统下会调用不同的 cmpxchg 重载函数,我当初用的是 win10 零碎,所以咱们看看这个平台下的实现,别着急再往下走走:

       jint compare_value) {int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

这块的代码就有点波及到汇编指令相干的代码了,到这一步就彻底靠近假相了,首先 三个 move 指令 示意的是将前面的值挪动到后面的寄存器上。而后调用了 LOCK_IF_MP 和上面 cmpxchg 汇编指令进行了比拟替换。当初咱们不晓得这个 LOCK_IF_MPcmpxchg是如何替换的,没关系咱们最初再深刻一下。

假相来了,他来了,他真的来了。

                             volatile jint* dest, jint compare_value) {
  //1、判断是否是多核 CPU
  int mp = os::is_MP();
  __asm {
    //2、将参数值放入寄存器中
    mov edx, dest   
    mov ecx, exchange_value
    mov eax, compare_value 
    //3、LOCK_IF_MP 指令
    cmp mp, 0
    //4、如果 mp = 0,表明线程运行在单核 CPU 环境下。此时 je 会跳转到 L0 标记处,间接执行 cmpxchg 指令
    je L0
    _emit 0xF0
//5、这里真正实现了比拟替换
L0:
    /*
     * 比拟并替换。简略解释一下上面这条指令,相熟汇编的敌人能够略过上面的解释:
     *   cmpxchg: 即“比拟并替换”指令
     *   dword: 全称是 double word 示意两个字,一共四个字节
     *   ptr: 全称是 pointer,与后面的 dword 连起来应用,表明拜访的内存单元是一个双字单元 
     * 这一条指令的意思就是:将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值进行比照,如果雷同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中。*/
    cmpxchg dword ptr [edx], ecx
  }
}

到这一步了,置信你应该了解了这个 CAS 真正实现的机制了吧,最终是由操作系统的汇编指令实现的。

3、CAS 机制的优缺点

(1)长处

一开始在文中咱们已经提到过,cas 是一种乐观锁,而且是一种非阻塞的轻量级的乐观锁 ,什么是非阻塞式的呢?其实就是一个线程想要取得锁,对方会给一个回应示意这个锁能不能取得。在资源竞争不强烈的状况下性能高,相比synchronized 分量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作。

(2)毛病

毛病也是一个十分重要的知识点,因为波及到了一个十分驰名的问题,叫做 ABA 问题。假如一个变量 A,批改为 B 之后又批改为 A,CAS 的机制是无奈觉察的,但实际上曾经被批改过了。这就是 ABA 问题,

ABA 问题会带来大量的问题,比如说数据不统一的问题等等。咱们能够举一个例子来解释阐明。

你有一瓶水放在桌子上,他人把这瓶水喝完了,而后从新倒上去。你再去喝的时候发现水还是跟之前一样,就误以为是刚刚那杯水。如果你晓得了假相,那是他人用过了你还会再用嘛?举一个比拟黄一点的例子,女朋友被他人睡过之后又回来,还是之前的那个女朋友嘛

ABA 能够有很多种形式来解决,在其余的文章曾经给出。

正文完
 0