谈谈JUC----------CAS机制及AtomicInteger源码分析

1,715 阅读6分钟

一、CAS简介

CAS即Compare And Swap对比交换,区别于悲观锁,借助CAS可以实现区别于synchronized独占锁的一种乐观锁,被广泛应用在各大编程语言之中。Java JUC底层大量使用了CAS,可以说java.util.concurrent完全是建立在CAS之上的。但是CAS也有相应的缺点,诸如ABAcpu使用率高等问题无法避免。

CAS总共有3个操作数,当前内存值V,旧的预期值A,要修改的新值N。当且仅当A和V相同时,将V修改为N,否则什么都不做。

二、CAS源码分析

我们都知道,java提供了一些列并发安全的原子操作类,如AtomicIntegerAtomicLong.下面我们拿AtomicInteger为例分析其源码实现。

// 1、获取UnSafe实例对象,用于对内存进行相关操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 2、内存偏移量
private static final long valueOffset;

static {
    try {
        // 3、初始化地址偏移量
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

// 4、具体值,使用volatile保证可见性
private volatile int value;

从上面代码中我们可以看出,AtomicInteger中依赖于一个叫Unsafe的实例对象,我们都知道,java语言屏蔽了像C++那样直接操作内存的操作,程序员不需手动管理内存,但话说回来,java还是开放了一个叫Unsafe的类直接对内存进行操作,由其名字可以看出,使用Unsafe中的操作是不安全的,要小心谨慎。

valueOffset是对象的内存偏移地址,通过Unsafe对象进行初始化,有一点需要注意的是,对于给定的某个字段都会有相同的偏移量,同一类中的两个不同字段永远不会有相同的偏移量。也就是说,只要对象不死,这个偏移量就永远不会变,可以想象,CAS所依赖的第一个参数(内存地址值)正是通过这个地址偏移量进行获取的。

value属于共享资源,借助volatile保证内存可见性,关于volatile的简单分析,可以参考

Java 反汇编、反编译、volitale解读

// 1、获取并增加delta
public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 2、加一
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

上面两个方法依赖下面Unsafe类中的getAndAddInt操作,借助openjdk提供的Unsafe源码,我们看下其实现:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    // 1、不断的循环比较,直到CAS操作成功返回
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

从上面可以看出,本质上CAS使用了自旋锁进行自旋,直到CAS操作成功,如果很长一段时间都没有操作成功,那么将一直自旋下去。

三、CAS的缺点

从第一、二节可以看出,CAS在java中的实现本质上是使用Unsafe类提供的方法获取对象的内存地址偏移量,进而通过自旋实现的。CAS的优点很明显,那就是区别于悲观策略,乐观策略在高并发下性能表现更好,当然CAS也是有缺点的,主要有类似ABA自旋时间过长只能保证一个共享变量原子操作三大问题,下面我们一一分析。

1、ABA

什么是ABA呢?简单的说,就是有两个线程,线程A和线程B,对于同一个变量X=0,A准备将X置为10,按照CAS的步骤,首先会从内存读取值旧的预期值0,然后比较,最后置为10,但就在A读取完X=0后,还没来得及比较和赋值,此时线程B完成了X=0 -> X=10 -> X=0这3个操作,随后A继续执行比较,发现此时内存的值依旧是0,最后CAS执行成功。虽然过程和结果没有问题,但是A比较时的0已经不是最初那个0了,有种被偷梁换柱的感觉。

下面代码举例演示ABA问题,线程1模拟将变量从100->110->100,线程2执行100->120,最后看下输出:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 
 * @Author jiawei huang
 * @Since 2020年1月17日
 * @Version 1.0
 */
public class ABATest {
    // 初始值为100
    private static AtomicInteger atomicInteger = new AtomicInteger(100);
    public static void main(String[] args) throws InterruptedException {
    
    	// AtomicInteger实现 100->110->100
    	Thread t1 = new Thread(new Runnable() {
    		@Override
    		public void run() {
    			atomicInteger.compareAndSet(100, 110);
    			atomicInteger.compareAndSet(110, 100);
    		}
    	});
    
    	// 实现 100->120
    	Thread t2 = new Thread(new Runnable() {
    		@Override
    		public void run() {
    			try {
    				// 这里模拟线程1执行完毕,偷梁换柱成功
    				TimeUnit.SECONDS.sleep(2);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			// 下面依旧返回true
    			System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100, 120));
    		}
    	});
    
    	t1.start();
    	t2.start();
	}
}

输出结果为:

AtomicInteger:true

可见线程2中的CAS也执行成功了,那么如何解决这个问题呢?解决方案是通过版本号,Java提供了AtomicStampedReference来解决。AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。

/*
 * Copyright (C) 2011-2019 DL 
 * 
 * All right reserved.
 * 
 * This software is the confidential and proprietary information of DL of China.
 * ("Confidential Information"). You shall not disclose such Confidential 
 * Information and shall use it only in accordance with the argeements 
 * reached into with DL himself.
 *
 */
package com.algorithm.leetcode.linkedlist;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 
 * @Author jiawei huang
 * @Since 2020年1月17日
 * @Version 1.0
 */
public class ABATest {
    // 初始值100,版本号1
    private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100, 1);
    
    public static void main(String[] args) throws InterruptedException {
    // AtomicStampedReference实现
    Thread tsf1 = new Thread(new Runnable() {
    	@Override
    	public void run() {
    		try {
    			// 让 tsf2先获取stamp,导致预期时间戳不一致
    			TimeUnit.SECONDS.sleep(2);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		// 预期引用:100,更新后的引用:110,预期标识getStamp() 更新后的标识getStamp() + 1
    		atomicStampedReference.compareAndSet(100, 110, atomicStampedReference.getStamp(),
    				atomicStampedReference.getStamp() + 1);
    		atomicStampedReference.compareAndSet(110, 100, atomicStampedReference.getStamp(),
    				atomicStampedReference.getStamp() + 1);
    	}
    });
    
    Thread tsf2 = new Thread(new Runnable() {
    	@Override
    	public void run() {
        	int stamp = atomicStampedReference.getStamp();
        
        	try {
        		TimeUnit.SECONDS.sleep(2); // 线程tsf1执行完
        	} catch (InterruptedException e) {
        		e.printStackTrace();
        	}
        	System.out.println(
        			"AtomicStampedReference:" + atomicStampedReference.compareAndSet(100, 120, stamp, stamp + 1));
    	}
    });
    
    tsf1.start();
    tsf2.start();
	}
}

输出结果:

AtomicStampedReference:false

可以看出线程1执行失败了。

2、自旋时间过长

通过第二节分析可以得知,CAS本质上是通过自旋来判断是否更新的,那么问题来了,如果多次旧预期值不等于内存值的情况,那么这个自旋将会自旋下去,而自旋过久将会导致CPU利用率变高。

3、只能保证一个共享变量原子操作

从第二节可以看出,只是单纯对单个共享对象进行CAS操作,保证了其更新获取的原子性,无法对多个共享变量同时进行原子操作。这是CAS的局限所在,但JDK提供同时了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。