CAS

concurrent包下的类都有下面的实现模式
1、首先,声明变量为volatile
2、然后,使用CAS的原子条件更新来实现线程之间的同步
3、配合使用volatile的写可见性和CAS的原子性来实现线程之间的通信(线程安全)

最近在看 java.util.concurrent.atomic 包下的AtomicInteger源码发现它是利用CAS来实现原子操作、Volatile保证元素的可见性来实现无锁下的线程安全。  决定深入了解一下CAS
MySQL中的MVCC(多版本并发控制)中的乐观锁也是通过CAS机制和版本号实现无锁更新数据的
  

CAS:Campare And Swap 比较和交换,是一种无锁算法,在不使用锁的情况下实现多线程之间的变量同步。
  CAS(V,E,N) 的三个参数
  V   要修改的对象(内存地址)
  E   Expect 期望原值
  N   New 要修改为的新值

比较和交换的原子操作是通过CPU的cmpxchg指令来实现的。


流程:
1、获取要修改的变量的现有值
2、和期望值比较,是否相同
3、相同的话则将变量的值修改为新值并返回TRUE,否则什么都不做并返回FALSE。

  另外,还有许多CAS操作时自旋的,即如果操作不成功,会一直重试,直到操作成功为止(如JUC包中类对变量的修改操作)。




如AtomicInteger:
    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;

        // setup to use Unsafe.compareAndSwapInt for updates
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private static final long valueOffset;

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

        private volatile int value;
        
        /**
         * Atomically sets the value to the given updated value
         * if the current value {@code ==} the expected value.
         *
         * @param expect the expected value
         * @param update the new value
         * @return {@code true} if successful. False return indicates that
         * the actual value was not equal to the expected value.
         */
        public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }

    }

各属性的作用:

  unsafe: 获取并操作内存的数据(根据内存偏移量)。

  valueOffset: 存储value在AtomicInteger中的内存偏移量。

  value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的

 



a++的操作是非原子性的,它实际上是两个操作
  1)将a的值加一

  2)将加一后的值赋给a
因此在多线程环境下,可能存在下面的问题
1、在不用volatile修饰的情况下
  线程1获取a的值为1,线程2取a的值为1,分别对其进行a++操作,之后a的b不是3人、而是2
2、在用volatile修饰的情况下
  线程1获取a的值为1,线程2获取a的值为1
  线程1在对a加一,但未将加一的值赋给a之前,线程2完成了对a的加一并赋值的操作,此时变量a的值为2
  由于volatile修饰的a保证了可见性,此时线程1中的工作内存中的a值无效,重新从堆内存中获取变量a的值,然后完成将a的值赋值为2
  两个线程分别对变量a进行加一操作后,变量a的值不是期望的3,而是2,这是由于加一和赋值的操作不是原子性导致的,所以并发包内提供的原子性的自增方法
AtomicInteger:   
/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

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

但是Unsafe的使用是受限的,只有授信的代码才能使用,即JDK库里面的内才可以使用(其帮助Java访问操作系统底层资源,使Java具有底层操作能力,可以提升运行效率)
private static final Unsafe theUnsafe;

// 私有化构造函数

private Unsafe() {
}
//
只有此方法可以获取Unsafe对象
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}

但是可以通过使用反射来获取Unsafe对象

 Class unsafeClass = Unsafe.class;
        try {
            Field f = unsafeClass.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

 

Unsafe类的方法

 /**
     * CAS
     * @param var1 要修改field的对象
     * @param var2 对象中某field的偏移量
     * @param var4 期望值
     * @param var5 更新值
     * @return true|false
     */
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

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

    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

获取给定的paramField的内存地址偏移量,这个值对于给定的field是唯一且固定不变的

 public native int arrayBaseOffset(Class<?> var1);


CAS存在的三大问题

1、ABA问题

  ABA:例如两个线程同时更新同一个数据,线程1操作比较快,先把A修改为了B,然后再修改为了A;
  线程2读取到线程1两次修改后的值A和之前的期望值A比较,相同,则认为没有被修改过符合期望,接着修改为新值。
解决方案是引入版本的概念:如A1-B2-A3,这样,另一个线程读取到的A3和期望值A1虽然值相同但是版本不同,则不进行后续的操作。

JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。
compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值

2、循环时间长开销大
  C
AS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销

3、只能保证一个共享变量的原子操作
  
对一个共享变量执行操作时,CAS能够保证原子操作,但是同时对多个共享变量操作时,CAS是无法保证操作的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作



CAS如果保证原子性的?
CAS包含C和S两个操作,其能保证原子性是因为CAS是由底层CPU支持的原子操作(cmpxchg原子指令),其原子性是在硬件层面进行保证的。




END.


posted @ 2019-04-04 11:59  杨岂  阅读(218)  评论(0编辑  收藏  举报