5、CAS原子操作

 

1 CAS介绍


CAS,Compare And Swap,即比较并交换。Doug lea 大神在同步组件中大量使用 CAS 技术鬼斧神工地实现了 Java 多线程的并发操作。整个 AQS 同步组件、Atomic 原子类操作等等都是以 CAS 实现的。可以说 CAS 是整个 JUC 的基石。

CAS 比较交换的过程 CAS(V,A,B):

V-一个内存地址存放的实际值、A-旧的预期值、B-即将更新的值,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false。

CAS VS synchronized

  • synchronized 是线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的之后会阻塞其他线程获取该锁。
  • CAS(无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,所以出现冲突时就不会阻塞其他线程的操作,而是重试当前操作直到没有冲突为止。

2 解决原子性问题


如下代码,目的是启动 10 个线程,每个线程将 a 累加 1000 次,最终得到 a=10000。

public class CASTest {
	public int a = 0;

	public void increase() {
		a++;
	}

	public static void main(String[] args) {
		final CASTest test = new CASTest();
		for (int i = 0; i < 10; i++) {
			new Thread() {
				public void run() {
					for (int j = 0; j < 1000; j++)
						test.increase();
				};
			}.start();
		}

		while (Thread.activeCount() > 1) {
			// 保证前面的线程都执行完
			Thread.yield();
		}
		System.out.println(test.a);
	}
}

结果:每次运行结果都小于 10000。

原因分析:

当线程 1 将 a 加到 2 时,a=2 刷新到主内存;
线程 2 执行增加运算时,到主内存读取 a=2,此时线程 3 也要执行增加运算,也到主内存中读取到 a=2;
线程 2 和线程 3 执行的都是 a=2+1,将 a=3 刷新到主内存。
相当于两次加 1 运算只将 a 增加了 1,也就是说存在执行了多次加 1 运算却只是将 a 增加 1 的情况,所以 10000 次加 1 运算,得到的结果会小于 10000。

原子性问题,解决方案 synchronized 和 CAS。

解决方案一:synchronized 加锁

public synchronized void increase() {
    a++;
}

通过 synchronized 加锁之后,每次只能有一个线程访问 increase()方法,能够保证最终得到 10000。但是 synchronized 加锁是个重量级操作,程序执行效率很低。

解决方案二:CAS

public AtomicInteger a = new AtomicInteger();
public void increase() {
    a.getAndIncrement();
}

利用 CAS,保证 a=a+1 是原子性操作,最终得到结果 10000。

3 CAS 原理


探究 CAS 原理,其实就是探究上个例子中 a.getAndIncrement() 如何保证 a=a+1 是原子性操作,先通过源码看下。

AtomicInteger 类结构

public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

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

    private volatile int value;
}
  1. Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定内存的数据。

  2. 变量 valueOffset 表示该变量值在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的原值。

  3. 变量 value 用 volatile 修饰,保证了多线程之间的内存可见性。

a.getAndIncrement() 的实现如下

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

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

getIntVolatile(var1, var2):根据对象 var1 和对象中该变量地址 var2,获取变量的值 var5。

this.compareAndSwapInt(var1, var2, var5, var5 + var4);

  1. 根据对象 var1 和对象中该变量地址 var2 获取变量当前的值 value

  2. 比较 value 跟 var5,如果 value==var5,则 value=var5+var4 并返回 true。这步操作就是比较和替换操作,是原子性的

  3. 如果 value!=var5,则返回 false,再去自旋循环到下一次调用 compareAndSwapInt 方法。

可见,getAndIncrement() 的原子性是通过 compareAndSwapInt() 中的第二步比较和替换保证的,那么 compareAndSwapInt() 又是怎么保证原子性的呢?

compareAndSwapInt 方法是 JNI(Java Native InterfaceJAVA 本地调用),java 通过 C 来调用 CPU 底层指令实现的。

4 CAS 问题


4.1 ABA 问题

CAS 需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是 A,变成了 B,然后又变成了 A,那么在 CAS 检查的时候会认为没有改变,但是实质上它已经发生了改变,这就是 ABA 问题。

解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径 A->B->A 就变成了 1A->2B->3A。

在 java 1.5 后的 atomic 包中提供了 AtomicStampedReference 来解决 ABA 问题,解决思路就是这样的。

4.2 自旋时间过长

使用 CAS 时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销。

优化:限制 CAS 自旋的次数,例如 BlockingQueue 的 SynchronousQueue。

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

当对一个共享变量执行操作时 CAS 能保证其原子性,如果对多个共享变量进行操作,CAS 就不能保证其原子性。

解决方案:把多个变量整成一个变量

  1. 利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量,然后将这个对象做 CAS 操作就可以保证其原子性。atomic 中提供了 AtomicReference 来保证引用对象之间的原子性。

  2. 利用变量的高低位,如 JDK 读写锁 ReentrantReadWriteLock 的 state,高 16 位用于共享模式 ReadLock,低 16 位用于独占模式 WriteLock。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值