并发编程系列之原子操作实现原理

前言

上节我们讲了并发编程中最基本的两个元素的底层实现,同样并发编程中还有一个很重要的元素,就是原子操作,原子本意是不可以再被分割的最小粒子,原子操作就是指不可中断的一个或者一系列操作。那么今天我们就来看看在多处理器环境下Java是如何保证原子操作的,ok,开始我们今天的并发编程之旅吧。

 

处理器如何实现原子操作

处理器自身会自动保证基本的内存操作原子性,保证从系统内存中读取或者写入一个字节动作是原子性的,也就是说,当处理器读取一个字节时,其他处理器不能访问该字节的内存地址。但是处理器自身只能保证基本的内存操作原子性,对于复杂的操作例如跨总线、跨多个缓存行和跨页表的访问,处理器自身是无法保证原子操作的,只能通过下面两种机制来保证操作原子性:

 

使用总线锁保证原子性:假设多个处理器同时处理一个共享变量,如果没有锁机制,就会出现同一个共享变量同一时刻被多个处理器同时处理的情况,就会造成结果的不一致性,例如i=1,我们要进行2次i++,就会出现i=2和i=3两种结果情况。很明显这不是我们要的结果,所以要想保证读写共享变量的操作是原子性的,就必须保证当处理器1在操作共享变量时,处理器2不能操作缓存了该共享变量内存地址的缓存。

因此引出了总线锁,总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号,其他处理器的请求将被阻塞,那么该处理器就能独占共享内存;

可以想象为:公司有一个会议室(共享内存),各个部门(处理器)开会都在会议室进行,当有一个部门占用会议室时,就会在会议室门口或者公司群里通知会议室此时被占用,那么其他部门的会议就得先等着(阻塞),等该部门结束会议才能开始下一个会议。

 

总线锁的缺点:总线锁把CPU和内存之间的通信锁住了,在锁期间,其他处理器不能操作其他内存地址的数据,开销比较大。

  • 使用缓存锁保证原子性:缓存锁指的是内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上通知LOCK信号,而是修改内部的内存地址,然后通过它的缓存一致性机制来保证操作的原子性;

    缓存一致性机制:缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写被已经锁定的缓存行数据时,会使缓存行失效。

  • 两种情况下不使用缓存锁

    • 当操作的数据不能被缓存在处理器内部,或者操作的数据跨多个缓存行,此时直接用总线锁

    • 当处理器不支持缓存锁定时,即使锁定的内存区域在处理器缓存行中,也会直接使用总线锁

 

Java中如何实现原子操作

Java中主要通过下面两种方式来实现原子操作:锁和循环CAS

锁机制实现原子操作

锁机制保证了只有获得锁的线程才能操作锁定的内存区域,JVM内部实现了很多种锁,偏向锁、轻量级锁和互斥锁,等等,这里就先不对锁进行详细介绍了,偏向锁和轻量级锁,上文也有提到过,感兴趣的可以通过文章末尾点击查阅,这些锁中,除了偏向锁,其他锁的方式都使用了循环CAS,那么我们来看下CAS相关内容。

 

什么是CAS操作

CAS全称Compare-and-Swap(比较并交换),JVM中的CAS操作是依赖处理器提供的cmpxchg指令完成的,CAS指令中有3个操作数,分别是内存位置V、旧的预期值A和新值B。

 

CAS指令如何保证原子性

当CAS指令执行时,当且仅当内存位置V符合旧预期值时A时,处理器才会用新值B去更新V的值,否则就不执行更新,但是无论是否更新V,都会返回V的旧值,该操作过程就是一个原子操作,JDK1.5之后才可以使用CAS,由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等方法包装实现,虚拟机在即时编译时,对这些方法做了特殊处理,会编译出一条相关的处理器CAS指令;

但是由Unsafe方法是虚拟机自己调用的,不能直接供用户程序调用,我们只能通过J.U.C包下的类来间接调用,如AtomicInteger和AtomicLong类,这些类中的方法都以原子的方式来进行操作,例如AtomicInteger的incrementAndGet方法中就是使用了compareAndSet,compareAndSet和getAndIncrement等方法都是使用Unsafe的CAS操作;

public final int incrementAndGet() {
       for (;;) {
           int current = get();
           int next = current + 1;
           if (compareAndSet(current, next))
               return next;
       }
   }

先获取到当前的 value 属性值,然后将 value 加 1,赋值给一个局部的 next 变量,然而,这两步都是非线程安全的,但是内部有一个死循环(循环CAS或者称自旋CAS),不断去做compareAndSet操作,直到成功为止,也就是修改的根本在compareAndSet方法里面

public final boolean compareAndSet(int expect, int update) {
       return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
   }

compareAndSwapInt上面提到了是Unsafe类的方法,是基于CAS指令的,其实compareAndSwapInt方法的声明是一个native本地方法

publicfinal native boolean compareAndSwapInt(Object var1, long var2, int var4, intvar5);

 

CAS实现操作的三大问题

CAS虽然很好的解决了原子操作问题,但是仍然存在下面3种问题

ABA问题:使用CAS时因为会先去检查内存位置的旧值A有没有发生变化,发生变化则更新最新值B,但是存在一种情况就是,初次读取内存旧值时是A,再次检查之前这段期间,如果内存位置的值发生过从A变成B再变回A的过程,我们就会错误的检查到旧值还是A,认为没有发生变化,其实已经发生过A-B-A得变化,这就是CAS操作的ABA问题;

解决ABA问题的思路:使用版本号,即1A-2B-3A,这样就会发现1A到3A的变化,不存在ABA变化无感知问题,JDK的atomic包中提供一个带有标记的原子引用类AtomicStampedReference来解决ABA问题,它可以通过控制变量值得版本号来保证CAS的正确性。该类的compareAndSet方法会首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果都相等则以原子方式该引用和该标志的值进行更新。

循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销;

只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,可以使用循环CAS来保证原子操作,但是多个共享变量操作时,就无法保证了。

解决方法

  • 使用锁

  • 将多个变量组合成一个共享变量,jdk提供了AtomicReference类来保证引用对象之间的原子性,那么就可以把多个变量放在一个对象里来进行CAS操作

 

结合上节内容,我们就将并发底层最重要的三个元素实现原理讲了一遍,这里面或多或少有些概念或者操作,大家看得有些模糊,不用担心,后续我们还会继续探讨

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章