CAS和Atomic类详解

CAS和Atomic类

概述

CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。

案例

package com.test.cas;

import java.util.concurrent.CountDownLatch;

public class VolatileTest4 {
	private static volatile int race = 0;
	private static void incr() {
		race++;
	}
	public static void main(String[] args) throws InterruptedException {
		//CountDownLatch latch = new CountDownLatch(20);
		for (int i = 0; i < 20; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					for (int j = 0; j < 10000; j++) {
						incr();
					}
					
					//latch.countDown();
					
				}
			}).start();
		}
		
		// 等全部都运行完毕再继续执行主线程
		//latch.await();
		while(Thread.activeCount() >1) {
			Thread.yield();
		}
		
		System.out.println("race:" + race);
		
	}
}

可以确定,无论race字段加不加volitile,都会出问题,预期是200000,实际不是。这是因为race++是一个几个步骤的操作,无法保证这几个步骤的原子性的问题。

那如何改?
只要加synchronized即可,

private synchronized static void incr() {
	race++;
}

或者改用java.util.concurrent.atomic.AtomicInteger

package com.test.cas;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class VolatileTest3 {
	private static AtomicInteger race = new AtomicInteger(0);
	private static void incr() {
		race.addAndGet(1);
	}
	public static void main(String[] args) throws InterruptedException {
		//CountDownLatch latch = new CountDownLatch(20);
		for (int i = 0; i < 20; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					for (int j = 0; j < 10000; j++) {
						incr();
					}
					
					//latch.countDown();
					
				}
			}).start();
		}
		
		// 等全部都运行完毕再继续执行主线程
		//latch.await();
		while(Thread.activeCount() >1) {
			Thread.yield();
		}
		
		System.out.println("race:" + race);
		
	}
}

为什么AtomicInteger可以?
跟踪其代码

private static final Unsafe unsafe = Unsafe.getUnsafe();
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

再跟踪进去
image

可以看到接口的逻辑:

  1. 首先获取最新的v值(这个值就是预期值)
  2. 再传给 compareAndSwapInt 参数:预期值v,新值v+delta
  3. 如果所要修改的内存地址,其值等于预期v,说明中途未被其他线程修改,则用新值替换该内存地址上的值(即旧值),修改成功将预期值(即旧值)返回,至此结束。
  4. 如果其值不等于预期v,说明中途被修改了,则重复1、2、3直到成功为止

(这里补充下接口Object和offset字段的含义,应该指要修改o这个对象的offset字段,至于为什么叫offset,我估计是对象占用一定内存,用offset来确定这个字段在内存的哪个偏移量上)

CAS详解

CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

CAS的缺点

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

  1. 循环时间长开销很大。
  2. 只能保证一个共享变量的原子操作。
  3. ABA问题。

注意: 不断循环直至成功是sun.misc.Unsafe的API,不是CAS底层指令本身,底层的就操作一次,如果一次新值和预期不一样就不再尝试。是Unsafe的Java类自己实现的循环尝试,并且compareAndSwapXXX只尝试一次,只有getAndAddXXX和getAndSetXXX才实现了循环尝试

循环实际长开销大

如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销

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

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

ABA 问题

如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?

如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

参考资料

面试必问的CAS

猜你喜欢

转载自blog.csdn.net/w8y56f/article/details/89554154