关于CAS的一点理解和思考

文章目录

  • CAS
  • Java中的CAS操作
  • 加锁和CAS
    • 性能测试比较
  • CAS不同线程数量下的额外开销

CAS

CAS(Compare And Swap)是对一种处理器指令的称呼,中文译为:比较并交换。
它需要三个参数:内存地址V、期望的旧值A、要替换的新值B。
它要完成的功能:当且仅当内存地址V的值等于A时,将A替换为B并返回true,否则什么也不做直接返回false。
用Java代码描述,大致如下所示:

/**
 * @paramv 内存地址
 * @param a 期望旧值
 * @param b 替换的新值
 * @return
 */
boolean compareAndSwap(V v,A a,B b){
	if (v == a){
		v = b;
		return true;
	}
	return false;
}

可以看到CAS是一个典型的check-and-act操作,如果不加锁很明显它不是一个原子操作。

JUC下很多类都用到了大量的CAS操作,如:AQS、ConcurrentHashMap、atomic包下的原子变量类。
它们是如何做到在不加锁的情况下确保多线程安全的呢?


Java中的CAS操作

CAS操作依赖于现代CPU支持的并发原语,换句话说,整个“比较并交换”的过程CPU会保证它的原子性,执行过程中不会因为时间片用完而被中断。
因此Java语言本身是无法实现CAS操作的,需要借助JNI调用本地代码来实现。

在Java平台中,利用sun.misc.Unsafe类来完成CAS操作,查看源码会发现大多数方法都是被native修饰的,意味这Java需要调用其他语言的代码才能实现CAS操作。

正如它的名字一样,Unsafe是一个不安全的类,使用它可以直接操作内存地址、分配堆外内存。使用Unsafe分配的内存GC是不会自动回收的,因此一旦使用不当很容易造成内存泄漏,所以JDK对Unsafe类的使用做了一些限制。

Unsafe构造方法被私有化了,不能直接new实例,提供了一个获取实例的方法:getUnsafe(),源码如下:

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

public static boolean isSystemDomainLoader(ClassLoader var0) {
   return var0 == null;
}

要想拿到Unsafe实例,首先会检查调用类的类加载器是否为null,否则会抛出异常。
只有被Bootstrap ClassLoader类加载器加载的类才符合这个条件,意味着开发者自己编写的类是无法获取Unsafe实例的,因为Java压根就没打算让开发者去用它。

虽然不能通过正规渠道去使用Unsafe,但是可以借助Java的反射机制来获取。

  • 线程不安全的int自增示例:
public class UnsafeIncr {
	volatile int i = 0;//volatile能保证可见性,但是无法保证i++的原子性

	void incr(){
		i++;
	}

	public static void main(String[] args) throws InterruptedException {
		UnsafeIncr incr = new UnsafeIncr();
		//10线程 每个线程自增1万次 期望结果10万
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				for (int j = 0; j < 10000; j++) {
					incr.incr();
				}
			}).start();
		}

		//主线程休眠1s,等待10个线程执行结束
		Thread.sleep(1000);
		System.out.println(incr.i);
		//输出结果几乎总是小于10万
	}
}
  • 利用CAS实现的线程安全的int自增:
public class CASIncr {
	static final Unsafe unsafe;
	static final long fieldOffset;
	public int index = 0;

	static {
		try {
			//反射获取Unsafe实例
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			unsafe = (Unsafe) field.get(null);
			//计算 index属性相对于UnsafeDemo类的内存偏移量
			fieldOffset = unsafe.objectFieldOffset(CASIncr.class.getField("index"));
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage());
		}
	}

	//index利用CAS完成自增操作
	void incr(){
		int old;
		do {
			//获取旧值
			old = unsafe.getIntVolatile(this, fieldOffset);
			//CAS操作 如果内存地址值=old,说明没有被其他线程修改,将新值替换为old+1并返回true,否则返回false再次自旋
		} while (!unsafe.compareAndSwapInt(this, fieldOffset, old, old + 1));
	}
}

利用CAS实现的自增操作是线程安全的,输出结果总是正确的。


加锁和CAS

加锁是保证线程安全功能最强大,也是适用范围最广的一种解决方案。
但是仅对于简单变量的读写操作来加锁,未免显得有点“杀鸡焉用牛刀”。

CAS可以看成是一种乐观锁的实现机制,假定不存在多线程竞争,如果变量没有被其他线程修改过那么直接写入成功,否则自旋进行重试,直到成功为止。

加锁(不考虑锁膨胀,指重量级锁)是一种悲观锁机制,认为肯定会发生数据冲突,通过OS级别的互斥量来保证每次最多只有一个线程进入临界区,通过挂起和唤醒线程来保证数据安全。

在单线程下,CAS的效率肯定是最高的,由于没有线程竞争,每次写入都会成功,完全不需要重试。
但是在线程竞争比较激烈的情况下,需要进行多次重试才能写入成功,反而会浪费CPU的性能。

这也就是为什么JDK6中加入“自适应自旋锁”的原因,竞争不激烈CAS自旋重试比挂起再唤醒线程的效率高,竞争激烈就直接挂起线程,避免浪费CPU的性能。

性能测试比较

int index = 0,自增一亿次,分别使用synchronized和CAS测试,对比耗时:

public class SyncTest {
	private int index = 0;
	private final int count = 100000000;
	private long startTime = System.currentTimeMillis();

	synchronized void incr(){
		index++;
		if (index == count) {
			System.out.println(System.currentTimeMillis() - startTime);
			System.exit(1);
		}
	}
}

public class CASTest {
	//不用JDK提供的原子类,自己实现
	static final Unsafe unsafe;
	static final long fieldOffset;
	public int index = 0;
	private final int count = 100000000;
	private long startTime = System.currentTimeMillis();

	static {
		try {
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			unsafe = (Unsafe) field.get(null);
			fieldOffset = unsafe.objectFieldOffset(CASTest.class.getField("index"));
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage());
		}
	}

	//直接使用unsafe.getAndAddInt()性能更好,这里为了更好的理解CAS,故采用compareAndSwapInt()
	void incr(){
		int oldValue;
		do {
			//获取当前值
			oldValue = unsafe.getIntVolatile(this, fieldOffset);

			//如果内存地址V的值=oldValue,则替换为(oldValue+1)并返回true,否则什么也不做直接返回false(说明值已被其他线程修改),继续自旋。
			//依赖于现代CPU提供的并发原语,CPU会保证整个比较并交换的动作的原子性。
		} while (!unsafe.compareAndSwapInt(this, fieldOffset, oldValue, oldValue + 1));

		if (oldValue == count - 1) {
			System.out.println(System.currentTimeMillis() - startTime);
			System.exit(1);
		}
	}
}
线程数 Sync耗时(ms) CAS耗时(ms)
1 2456 1092
2 4183 5201
5 5633 8785

CAS不同线程数量下的额外开销

int index = 0,自增一亿次,分别测试CAS在不同数量线程的竞争下额外的自旋次数。

public class CAS {
	static final Unsafe unsafe;
	static final long fieldOffset;
	public int index = 0;
	private int count = 100000000;
	private long startTime = System.currentTimeMillis();//开始时间
	private AtomicInteger casCount = new AtomicInteger(0);//统计CAS次数的原子类

	static {
		try {
			//反射拿到Unsafe实例、获取index属性的偏移量
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			unsafe = (Unsafe) field.get(null);
			fieldOffset = unsafe.objectFieldOffset(CASTest.class.getField("index"));
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage());
		}
	}

	void incr(){
		while (true) {
			int old = unsafe.getIntVolatile(this, fieldOffset);
			//CAS自增成功,结束自旋
			if (unsafe.compareAndSwapInt(this, fieldOffset, old, old + 1)) {
				break;
			}else {
				//统计额外的自旋次数
				casCount.incrementAndGet();
			}
		}

		//自增一亿次结束,输出额外的自旋次数和耗时信息
		if (index == count) {
			System.out.println("额外的自旋次数:"+casCount.get());
			System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
			System.exit(1);
		}
	}

	public static void main(String[] args) {
		CAS cas = new CAS();
		for (int i = 0; i < 5; i++) {
			new Thread(()->{
				while (true) {
					cas.incr();
				}
			}).start();
		}
	}
}
线程数 额外自旋次数 耗时(ms)
1 0 1329
2 51905576 6585
5 147004339 11325
10 218429562 11438

单线程下,由于不存在竞争关系,每次写入都会成功,完全不需要自旋重试。
但是随着多线程竞争的激烈程度的上升,需要自旋重试的次数不断变多,性能也随之下降。

只做自增的话直接调用unsafe.getAndAddInt()可以获得更好的性能,本博客旨在帮助大家更好的理解CAS操作,遂取旧值再调用compareAndSwapInt()。

你可能感兴趣的:(#,多线程)