详解CAS技术

无锁

实现并发的几种方式:

1. 锁
2. CAS
3. ThreadLocal变量;可重入性代码
  • 锁是一种悲观的策略,锁认为每次临界区的操作都会产生冲突,所以当有多个线程需要同时访问临界区资源,锁会阻塞线程执行
  • 无锁是一种乐观的策略,它假设所有对资源的访问都是没有冲突的,因此所有线程都可以不用阻塞的持续执行,一旦遇到冲突,无锁采用CAS(Compare And Swap)来鉴别冲突,一旦检测到冲突,则重试当前操作直到没有冲突为止

交换技术(CAS)

  • CAS有两大好处

    1. 无锁的方式没有锁竞争带来的系统开销,也没有不同线程间频繁调度的开销,所以性能比锁更优越
    2. 对死锁免疫
  • 具体思想:

    CAS包含三个参数(V,expect,update),我认为这三个参数分别是:

    V表示当前内存中的值,而expect表示当前读取到的值,只有当当前读取到的值和内存中的值一致时,我们会把内存中的时更新为最新的值,即update;若不等,则说明该值被其他线程修改了

    每次CAS操作时,一定有一个线程会成功更新变量,而其他失败的线程不会挂起,而是允许继续重试,直到成功为止

无锁的线程安全整数:AtomicInteger

>在Java并发包中有这样一个包,java.util.concurrent.atomic,该包是对Java部分数据类型的原子封装,
>该包下的类都是使用CAS实现的
>在原有数据类型的基础上,提供了原子性的操作方法,保证了线程安全,实现了一些直接使用CAS操作的线程安全的类型
>下面以AtomicInteger为例,来看一下是如何实现的。

我们列举AtomicInteger下的incrementAndGet()方法,该方法使得当前值加1,返回新值:

public final int incrementAndGet() {  
for (;;) {  
    int current = get();  
    int next = current + 1;  
    if (compareAndSet(current, next))  
        return next;  
}  
}  

该方法incrementAndGet()方法实现了原子性的自增操作(++i不是原子性),内部就是使用CAS技术实现的

Volatile变量

private volatile int value;

代表AtomicInteger的当前实际取值,此外还有一个: private static final long valueOffset; 它保存着value字段在AtomicInteger对象中的偏移量。

Compare And Swap


// 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"));// 反射出value属性,获取其在内存中的位置  
	  } catch (Exception ex) { throw new Error(ex); }  
	}  
	  
	public final boolean compareAndSet(int expect, int update) {  
	  return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
	}  
比较并设置,这里利用Unsafe类的JNI方法实现,使用CAS指令,可以保证读-改-写是一个原子操作。
public final native boolean compareAndSwapInt(Object o,long offset,int expected,int x);

compareAndSwapInt有4个参数,this - 当前AtomicInteger对象,Offset - value属性在内存中的位置(需要强调的是value值在内存中的位置),expect - 预期值,update - 新值,根据上面的CAS操作过程,当内存中的value值等于expect值时,则将内存中的value值更新为update值,并返回true,否则返回false。在这里我们有必要对Unsafe有一个简单点的认识,从名字上来看,不安全,确实,这个类是用于执行低级别的、不安全操作的方法集合,这个类中的方法大部分是对内存的直接操作,所以不安全,但当我们使用反射、并发包时,都间接的用到了Unsafe。

public final int incrementAndGet() {  
    for (;;) {  
        int current = get();  
        int next = current + 1;  
        if (compareAndSet(current, next))  
            return next;  
    }  
    }  

这样我们再来看之前提到的incrementAndGet()方法,循环内,获取当前值并设置更新值,调用compareAndSet进行CAS操作,如果成功就返回更新值,否则重试到成功为止。这里可能存在一个隐患,那就是循环时间过长,总是在当前线程compareAndSet时,有另一个线程设置了value(点子太背了),这个当然是属于小概率时间,目前Java貌似还不能处理这种情况。

CAS缺点

  1. ABA问题:当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了两次,而经过两次修改后,对象的值又恢复为旧值,这样当前线程无法正确判断这个对象是否修改过
    • 解决办法:JDK1.5可以利用类AtomicStampedReference来解决这个问题,AtomicStampedReference内部不仅维护了对象值,还维护了一个时间戳。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳,对象值和时间戳都必须满足期望值,写入才会成功
  2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
    • 解决办法:JVM支持处理器提供的pause指令,使得效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
  3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性
    • 解决办法:从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。





猜你喜欢

转载自blog.csdn.net/caisongcheng_good/article/details/80588525