Java多线程高并发(一) CAS原理

一 问题引入

       当我们测试多个线程操作a++的时候,会出现以下结果

public class CasDemo2 {
    public static void main(String[] args) {
        Castest castest=new Castest();
        for(int i=0;i<10;i++){
                Thread thread=new Thread(castest);
                thread.start();

        }
    }

}

class Castest implements Runnable{

    private   int a=0;
    @Override
    public void run() {
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+":"+a++);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果并没有按照我们想要的输出,我们将a++这个操作其实可以细分成三个步骤:

(1)从内存中读取a

(2)对a进行加1操作

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

发现6出现了三次,说明线程3,9,4获取a的时候,并不是最新的a。此时你肯定会想到volitile关键字,private  volatile int a=0;

但是结果还是会出现这种情况,可以自行尝试下。我们都知道 volitile具有内存可见性,即线程Avolatile变量的修改,其他线程获取的volatile变量都是最新的。但是volatile只提供了保证访问该变量时,每次都是从内存中读取最新值,并不会使用寄存器缓存该值——每次都会从内存中读取。而对该变量的修改,volatile并不提供原子性的保证。后续会对volitile进行详细的说明。

此时当然可以通过synchronized来保证线程安全性

class Castest implements Runnable{

    private volatile  int a=0;
    @Override

    public void run() {
        try {
            synchronized (this){
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+":"+a++);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

private 输出结果:

synchronized是一种独占锁,也叫悲观锁会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。

二 jdk的cas实现

我们可以通过jdk源码来分析cas的原理, 首先针对上面的线程安全性,是如何通过cas实现的呢,在jdk1.5之后有个Atomic包,可以通过该包下面的方法实现

public class CasDemo2 {
    public static void main(String[] args) {
        Castest castest=new Castest();
        for(int i=0;i<10;i++){
            Thread thread=new Thread(castest);
            thread.start();

        }
    }

}

class Castest implements Runnable{

   AtomicInteger count = new AtomicInteger();

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName()+":"+count.getAndIncrement());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

   测试输出结果:

  我们可以看到结果,在多线程并发情况下,并没有出现重复值的情况,每个线程拿到的都是不重复的值。看到这里有些人可能会有疑惑,atomic输出并没有像synchronized那样按顺序输出,为什么说是保证线程安全性。你可能对线程安全性有个误解,

所谓的线程安全性说简单点就是保证数据的正确性,跟顺序关系不大,要想保证线程按顺序执行方法很多,比如 线程的wait方法,join方法,wait方法,加锁等等。举个生活中的例子,比如a,b,c三人去购物,某个商品的库存只有10件,不论a,b,c谁先买,库存的逻辑正确性不会变,a买了2个,那么b,c只能有8件可以买。跟谁先买后买没关系,但是一定要保证这个操作的正确性。

三 分析cas源码

点击AtomicInteger的getAndIncrement方法,可以看到如下

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

可以看到主要两个方法,getIntVolatile与compareAndSwapInt方法,这两个方法很好的实现了线程安全性

保证原子性的策略:

1:变量都是用Volatile关键字修饰。来保证内存可见性(getIntVolatile)

2:使用CAS算法,来保证原子性。(compareAndSwapInt)

关于volatile更好的说明可以查看这边博客:java内存模型以及volatile

Cas算法源码:

public final native boolean compareAndSwapInt(
        Object var1,//操作的对象a
        long var2,//对象a的地址偏移量
        int var4,//对象a的期望值
        int var5 //对象a的实际值 
);

这个方法是native,调用C++层JVM的源码。这里有JVM的实现源码下载

链接:https://pan.baidu.com/s/1wRVNciNbT7ABGTPbR8Qlqw 
提取码:lekq 
Unsafe:

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

Unsafe.cpp:

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;
UNSAFE_END

核心方法就是cmpxchg(含义:compare and exchange)

由于这个有多个系统的实现,这里只看linux_x86架构

atomic_linux_x86.inline.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

这里使用了底层汇编语言,LOCK_IF_MP命令:根据当前系统是否为多核处理器决定是否为cmpxchg指令添加lock前缀。

lock的功能:

保证指令的执行的原子性
带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。
禁止该指令与之前和之后的读和写指令重排序
在AI-32架构软件开发者手册第8中内存排序中,有说明LOCK前缀会禁止指令与之前和之后的读和写指令重排序。这相当于JMM中定义的StoreLoad内存屏障的效果。也正是因为这个内存屏障的效果,会使得线程把其写缓冲区中的所有数据刷新到内存中。注意,这里不是单单被修改的数据会被回写到主内存,而是写缓存中所有的数据都回写到主内存。
而将写缓冲区的数据回写到内存时,就会通过缓存一致性协议(如,MESI协议)和窥探技术来保证写入的数据被其他处理器的缓存可见。
而这就相当于实现了volatile的内存语义。是的,上面我们为说明的lock前缀是如何实现volatile的内存语义就是这么保证的。

cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory

cmpxchgl的详细执行过程:
首先,输入是"r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp),表示compare_value存入eax寄存器,而exchange_value、dest、mp的值存入任意的通用寄存器。嵌入式汇编规定把输出和输入寄存器按统一顺序编号,顺序是从输出寄存器序列从左到右从上到下以“%0”开始,分别记为%0、%1···%9。也就是说,输出的eax是%0,输入的exchange_value、compare_value、dest、mp分别是%1、%2、%3、%4。
因此,cmpxchgl %1,(%3)实际上表示cmpxchgl exchange_value,(dest),此处(dest)表示dest地址所存的值。需要注意的是cmpxchgl有个隐含操作数eax,其实际过程是先比较eax的值(也就是compare_value)和dest地址所存的值是否相等,如果相等则把exchange_value的值写入dest指向的地址。如果不相等则把dest地址所存的值存入eax中。
输出是"=a" (exchange_value),表示把eax中存的值写入exchange_value变量中。
Atomic::cmpxchg这个函数最终返回值是exchange_value,也就是说,如果cmpxchgl执行时compare_value和dest指针指向内存值相等则会使得dest指针指向内存值变成exchange_value,最终eax存的compare_value赋值给了exchange_value变量,即函数最终返回的值是原先的compare_value。此时Unsafe_CompareAndSwapInt的返回值(jint)(Atomic::cmpxchg(x, addr, e)) == e就是true,表明CAS成功。如果cmpxchgl执行时compare_value和(dest)不等则会把当前dest指针指向内存的值写入eax,最终输出时赋值给exchange_value变量作为返回值,导致(jint)(Atomic::cmpxchg(x, addr, e)) == e得到false,表明CAS失败。

四 cas原理

CAS算法图解

CAS具体执行时,当且仅当预期值A符合内存地址V中存储的值时,就用新值U替换掉旧值,并写入到内存地址V中。否则不做更新。

CAS会有如下三个方面的问题:

1.ABA问题,一个线程将内存值从A改为B,另一个线程又从B改回到A。

2.循环时间长开销大:CAS算法需要不断地自旋来读取最新的内存值,长时间读取不到就会造成不必要的CPU开销。
3. 只能保证一个共享变量的原子操作(jdk的AtomicReference来保证应用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作,解决了这一问题)。

ABA问题图解

ABA问题解决方案:在变量前面添加版本号,每次变量更新的时候都将版本号加1,比如juc的原子包中的AtomicStampedReference类。

 

参考资料:https://www.cnblogs.com/wildwolf0/p/11455796.html

               https://www.jianshu.com/p/bd68ddf91240

   

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