原子操作的实现:CAS与锁

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

首先什么是原子操作?

原子本意是“不能被进一步分割的最小粒子”,而原子操作意为”不可被中断的一个或一系列操作”;

处理器如何实现原子操作?

  1. 首先处理器会自动保证基本的内存操作的原子性:处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。
  2. 总线锁保证原子性:所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
  3. 缓存锁定保证原子性:锁住总线的消耗太大,于是有了缓存锁。内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言Lock#信号,而是修改内部的内存地址,利用缓存一致性机制来保证操作的原子性。缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域的数据。

以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。

JAVA如何实现原子操作?

JAVA使用 循环CAS
二者还有另一个叫法:悲观锁与乐观锁:

  • 悲观锁: 假定会发生并发冲突,即共享资源会被某个线程更改。所以当某个线程获取共享资源时,会阻止别的线程获取共享资源。也称独占锁或者互斥锁,例如java中的synchronized同步锁。
  • 乐观锁: 假设不会发生并发冲突,只有在最后更新共享资源的时候会判断一下在此期间有没有别的线程修改了这个共享资源。如果发生冲突就重试,直到没有冲突,更新成功。CAS就是一种乐观锁实现方式。

悲观锁会阻塞其他线程。乐观锁不会阻塞其他线程,如果发生冲突,采用死循环的方式一直重试,直到更新成功。

CAS,Compare and Swap

如果当前状态值等于预期值,则以原子方式将同步状态设定为给定的更新值。

CAS同时具有volatile读和volatile写的内存语义。

volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷到主内存中。

volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

从编译器与处理器的角度来分析,CAS如何同时具有volatile读和volatile写的内存语义:

1,编译器不会对volatile写与volatile写前面的任意内存操作重排序,不会对volatile读与volatile读后面的任意内存操作重排序。那么同时具有volatile读和volatile写的内存语义,意味着1,编译器不能对CASCAS前面后面的任意内存操作重排序。

2,以X86处理器为例:实现依赖的是CMPXCHG指令,程序会根据处理器类型来决定是否为该指令添加lock前缀。类型指的是多处理器还是单处理器,对于多处理器会添加lock前缀。

intel手册对lock前缀的说明:

1,确保对内存的读-改-写操作原子执行。通过锁总线或缓存锁定的方式实现。

2,禁止改指令与之前和之后的读写指令重排序。

3,将写缓冲区中的所有数据刷新到主存中。

第2和3点具有内存屏障效果,足以同时实现volatile读和写的内存语义。


先来看看CAS在atomic类中的应用

    public final native boolean compareAndSwapObject
       (Object obj, long valueOffset, Object expect, Object update);

    public final native boolean compareAndSwapInt
       (Object obj, long valueOffset, int expect, int update);

    public final native boolean compareAndSwapLong
      (Object obj, long valueOffset, long expect, long update);

atomic类,它们实现了对确认,更改再赋值操作的原子性;我们说线程安全,当A线程在运行时被挂起,B线程运行后再回到A,此时B的修改对A不可见,那么便会出现问题。原先我们可以用Synchronized将代码保护起来,利用排他性,但是过于笨重,于是有了CAS,直接将原先确认操作得到的值再对它进行相关计算前,先到主内存中查看是否有被改变过,有则重新获取该值。这样同样解决了上面说的问题。

下面以AtomicInteger为例:

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
  • Unsafe是CAS的核心类,Java没有方法能访问底层系统,因此需要本地方法来做,Unsafe就是一个后门,被提供来直接操作内存中的数据。
  • valueOffset:变量在内存中的偏移地址,Unsafe根据偏移地址找到获取数据。
  • value被volatile修饰,保证了内存可见性。

getAndAdd方法

    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
//---------------unsafe
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

getIntVolatile通过偏移量获取到内存中变量值,compareAndSwapInt会比较获取的值与此时内存中的变量值是否相等,不相等则继续循环重复。整个过程利用CAS保证了对于value的修改的并发安全。

CAS存在的问题:

1,ABA问题:比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。
AtomicStampedReference来解决ABA问题:这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(
               V      expectedReference,//预期引用
               V      newReference,//更新后的引用
              int    expectedStamp, //预期标志
              int    newStamp //更新后的标志

)

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

3,只能保证一个共享变量的原子操作。对于多个变量可以放在一个对象里,通过AtomicReference来保证引用对象之间的原子性

使用锁机制实现原子操作

只有获得锁的线程才能够操作锁定的内存区域。除了偏向锁外,JVM实现锁的方式都用了循环CAS,即当一个线程想要进入同步块的时候使用循环CAS的方式来获取锁,当它退出的时候使用循环CAS来释放锁。关于Synchronized之后文章再详细介绍。

参考资料

《Java并发编程的艺术》

聊聊并发(五)原子操作的实现原理
深入浅出 Java Concurrency (5): 原子操作 part 4
深入浅出CAS

展开阅读全文
博主设置当前文章不允许评论。

没有更多推荐了,返回首页