6、CAS原理及其在 java中实例AutomicInteger

1.cas解释

比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

在多线程下会让 CPU 不断的切换,非常消耗资源,我们知道可以使用具体的某一类锁来避免部分问题。那除了锁的方式还有其他的吗?当然,有人就提出了无锁算法,比较有名的就是我们今天要说的 CAS(compare and swap),和锁不同的是它是一种乐观的机制,它认为别人去拿数据的时候不会修改,但是在修改数据的时候去判断一下数据此时的状态,这样的话 CPU 不会切换,在读多的情况下性能将得到大幅提升。

CAS 给我们提供了一种思路,通过 比较 和 替换 来完成原子性,来看一段代码:

1 int cas(long *addr, long old, long new) {
2    /* 原子执行 */
3    if(*addr != old)
4        return 0;
5    *addr = new;
6    return 1;
7 }

这是一段 c 语言代码,可以看到有 3 个参数,分别是:

  • *addr: 进行比较的值
  • old: 内存当前值
  • new: 准备修改的新值,写入到内存

只要我们当前传入的进行比较的值和内存里的值相等,就将新值修改成功,否则返回 0 告诉比较失败了。学过数据库的同学都知道悲观锁和乐观锁,乐观锁总是认为数据不会被修改。基于这种假设 CAS 的操作也认为内存里的值和当前值是相等的,所以操作总是能成功,我们可以不需要加锁就实现多线程下的原子性操作。

在多线程情况下使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被阻塞挂起,而是告诉它这次修改失败了,你可以重新尝试,于是可以写这样的代码。

1while (!cas(&addr, old, newValue)) {
2
3}
4// success
5printf("new value = %ld", addr);

2.Java 里的 CAS AutomicInteger

AtomicInteger 源码

1public class AtomicInteger extends Number implements java.io.Serializable {
 2    private static final long serialVersionUID = 6214790243416807050L;
 3
 4    // setup to use Unsafe.compareAndSwapInt for updates
 5    private static final Unsafe unsafe = Unsafe.getUnsafe();
 6    private static final long valueOffset;
 7
 8    static {
 9        try {
10            valueOffset = unsafe.objectFieldOffset
11                (AtomicInteger.class.getDeclaredField("value"));
12        } catch (Exception ex) { throw new Error(ex); }
13    }
14
15    private volatile int value;
16
17    /**
18     * Creates a new AtomicInteger with the given initial value.
19     *
20     * @param initialValue the initial value
21     */
22    public AtomicInteger(int initialValue) {
23        value = initialValue;
24    }
25
26    /**
27     * Gets the current value.
28     *
29     * @return the current value
30     */
31    public final int get() {
32        return value;
33    }
34
35    /**
36     * Atomically increments by one the current value.
37     *
38     * @return the updated value
39     */
40    public final int incrementAndGet() {
41        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
42    }
  • valueOffset: 在类初始化的时候,计算出value变量在对象中的偏移
  • value: 保存当前的值

getIntVolatile方法native实现

jint sun::misc::Unsafe::getIntVolatile (jobject obj, jlong offset)    
{    
  volatile jint *addr = (jint *) ((char *) obj + offset);    //3
  jint result = *addr;    //4
  read_barrier ();    //5
  return result;    //6
}  
inline static void read_barrier(){
  __asm__ __volatile__("" : : : "memory");
}
1.通过volatile方法获取当前内存中该对象的value值。
2. 计算value的内存地址。
3. 将值赋值给中间变量result。
4.插入读屏障,保证该屏障之前的读操作后后续的操作可见。
5. 返回当前内存值
6. 通过compareAndSwapInt操作对value进行+1操作,如果再执行该操作过程中,内存数据发生变更,则执行失败,但循环操作直至成功。

可以看到 incrementAndGet 调用了 unsafe.getAndAddInt 方法。Unsafe 这个类是 JDK 提供的一个比较底层的类,Java不像c或者c++那样,可以直接操作内存,Unsafe可以说是一个后门,可以直接操作内存,或者进行线程调度。Unsafe作用:

  • 内存管理:包括分配内存、释放内存
  • 操作类、对象、变量:通过获取对象和变量偏移量直接修改数据
  • 挂起与恢复:将线程阻塞或者恢复阻塞状态
  • CAS:调用 CPU 的 CAS 指令进行比较和交换
  • 内存屏障:定义内存屏障,避免指令重排序

下面我们继续看 unsafe 的 getAndAddInt 在做什么。

1public final int getAndAddInt(Object var1, long var2, int var4) {
 2    int var5;
 3    do {
 4        var5 = this.getIntVolatile(var1, var2);
 5    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
 6
 7    return var5;
 8}
 9
10public native int getIntVolatile(Object var1, long var2);
11public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

其实很简单,先通过 getIntVolatile 获取到内存的当前值,然后进行比较,展开 compareAndSwapInt 方法的几个参数:

  • var1: 当前要操作的对象(其实就是 AtomicInteger 实例)
  • var2: 当前要操作的变量偏移量(可以理解为 CAS 中的内存当前值)
  • var4: 期望内存中的值
  • var5: 要修改的新值

所以 this.compareAndSwapInt(var1, var2, var5, var5 + var4) 的意思就是,比较一下 var2 和内存当前值 var5 是否相等,如果相等那我就将内存值 var5 修改为 var5 + var4(其实并不是直接修改var5,而是对var1即对象进行修改)。

compareAndSwapInt源码

jboolean sun::misc::Unsafe::compareAndSwapInt (jobject obj, jlong offset,jint expect, jint update)  {  
  jint *addr = (jint *)((char *)obj + offset); //1
  return compareAndSwap (addr, expect, update);
}  

static inline bool compareAndSwap (volatile jlong *addr, jlong old, jlong new_val)    {    
  jboolean result = false;    
  spinlock lock;    //2
  if ((result = (*addr == old)))    //3
    *addr = new_val;    //4
  return result;  //5
} 

1 通过对象地址和value的偏移量地址,来计算value的内存地址。
2 使用自旋锁来处理并发问题。
3 比较内存中的值与调用方法时调用方所期待的值。
4 如果3中的比较符合预期,则重置内存中的值。
5 如果成功置换则返回true,否则返回false;
  • 所以getAndAddInt返回var5还是当前值,所以getAndIncrement返回的还是当前值,而incrementAndGet对返回值做了+1,返回了+1后的值。

3.CAS存在的问题-ABA问题

设想如下场景:

  • 线程1准备用CAS将变量的值由A替换为B。
  • 在此之前,线程2将变量的值由A替换为C,又由C替换为A,
  • 然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。

但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。

比如:

  • 有一个用单向链表实现的栈,栈顶为A。
  • 线程T1获取A.next为B,然后希望用CAS将栈顶替换为B,head.compareAndSet(A,B)。
  • 在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A。此时B.next为null。
  • 此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B。
  • 但实际上B.next为null,其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,这样就造成C、D被抛弃的现象。

解决方法

各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。

如果你做过数据库相关的乐观锁机制可能会想到我们在比较的时候使用一个版本号 version 来进行判断就可以搞定。在 JDK 里提供了一个 AtomicStampedReference 类来解决这个问题,来看一个例子:

1int stamp = 10001;
2
3AtomicStampedReference stampedReference = new AtomicStampedReference<>(0, stamp);
4
5stampedReference.compareAndSet(0, 10, stamp, stamp + 1);
6
7System.out.println("value: " + stampedReference.getReference());
8System.out.println("stamp: " + stampedReference.getStamp());

4.CAS应用

既然 CAS 提供了这么好的 API,我们不妨用它来实现一个简易版的独占锁。思路是当某个线程进入 lock 方法就比较锁对象的内存值是否是 false,如果是则代表这把锁它可以获取,获取后将内存之修改为 true,获取不到就自旋。在 unlock 的时候将内存值再修改为 false 即可,代码如下:

 1public class SpinLock {
 2
 3    private AtomicBoolean mutex = new AtomicBoolean(false);
 4
 5    public void lock() {
 6        while (!mutex.compareAndSet(false, true)) {
 7            // System.out.println(Thread.currentThread().getName()+ " wait lock release");
 8        }
 9    }
10
11    public void unlock() {
12        while (!mutex.compareAndSet(true, false)) {
13            // System.out.println(Thread.currentThread().getName()+ " wait lock release");
14        }
15    }
16
17}

这里使用了 AtomicBoolean 这个类,当然用 AtomicInteger 也是可以的,因为我们只保存一个状态 boolean 占用比较小就用它了。这个锁的实现比较简单,缺点非常明显,由于 while 循环导致的自旋会让其他线程都在占用 CPU,但是也可以使用,关于锁的优化版本实现我会在后续的文章中进行改进和说明,正因为这些问题我们也会在后续研究 AQS 这把利器的优点。

5.段地址和偏移地址

数据是储存在内存里的,你说的物理地址其实就是实际内存地址。也叫实际地址或绝对地址,是CPU访问存储器时实际使用的地址,为20位地址。

逻辑地址 是程序中使用的地址,它由段基址和段内偏移值所组成,段基址与段内偏移值都为16位的二进制数。

段地址指的是段起始地址的高16位

偏移地址指的是段内相对于段起始地址的偏移值(16位),偏移地址是相对而言的


image

你可能感兴趣的:(6、CAS原理及其在 java中实例AutomicInteger)