CAS

CAS原理

CAS(Compare And Swap)是用于多线程同步的原子性指令,CAS主要通过三个值来实现,原始内存的值,给定的预期值,给定更新值,只有当原始内存的值与给定的预期值相等的情况下,才将更新值返回。

假设有两个线程A,B同时执行a=a+1

根据JMM内存模型,A,B线程分别拷贝一份副本到自己的内存中,假设此时A线程挂起,线程B对a的值进行修改,线程B首先会判断拷贝的副本值是否与共享内存的值相等,如果相等,对a进行更新,并写回主内存,此时线程A要修改a了,但是线程A发现拷贝的副本值与共享内存中的值不同,线程A会重新从共享内存中拷贝副本进行修改。

原子类

  • 基本类型:AtomicInteger AtomicLong AtomicBoolean
  • 数组类型:AtomicIntegerArray AtomicLongArray AtomicReferenceArray
  • 引用类型:AtomicReference

底层是通过sun.misc.Unsafe+CAS思想来实现的

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

Unsafe中的类都是native方法,它可以通过valueOffset拿到在内存中的值,然后利用一个do…while循环,判断内存中的值是否与当前的相等,如果相等则对值进行更新,并将更新后的值返回,否则,进入循环中,重新获取内存值的最新值。

public final int getAndAddInt(object o,long offset,int delta){
    int v;
    do{
        v = getIntVolatile(o,offset);//拿到内存位置的最新值
    }while(!compareAndSwapInt(o.offset,v,v+delta))//o.offset的值 == v
}

CAS缺点

  • 如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功。可能会给CPU带来负担
  • 只能保证一个共享变量的原子操作
  • ABA问题

ABA问题

假设有两个线程t1,t2一个共享变量A,线程t1,t2分别将A拷贝一份到自己的内存,这时t1先将A的值改为B

,再将B的值改为A,这时线程T2要修改值,发现符合CAS,对A的值进行更新。这就是ABA问题,线程t1对A的值进行过修改,线程t2完全忽略了这个过程。

AtomicReference ar = new AtomicReference(100);
	// 线程t1
		new Thread(()->{
			ar.compareAndSet(100, 101);
			ar.compareAndSet(101,100);
		},"t1").start();
	// 线程t2
		new Thread(()->{
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("是否修改成功:"+ar.compareAndSet(100, 101)+"\t当前最新值为"+ar.get());
		},"t2").start();

打印结果 :

是否修改成功:true 当前最新值为101

解决方法

使用原子引用+版本号控制

// 初始值为 100,stamp为 1
		AtomicStampedReference asr = new AtomicStampedReference(100, 1);
// 线程t1执行ABA操作,版本更新
		new Thread(()->{
			int stamp = asr.getStamp();
			System.out.println("当前线程"+Thread.currentThread().getName()+"的版本号"+stamp);
			try {
				//等待线程t2拿到版本号
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			asr.compareAndSet(100, 101, asr.getStamp(), asr.getStamp()+1);
			System.out.println(Thread.currentThread().getName()+"\t"+"当前版本号"+asr.getStamp());
			asr.compareAndSet(101, 100, asr.getStamp(), asr.getStamp()+1);
			System.out.println(Thread.currentThread().getName()+"\t"+"当前版本号"+asr.getStamp());
		},"t1").start();
// 线程t2修改值
		new Thread(()->{
			int stamp = asr.getStamp();
			System.out.println("当前线程"+Thread.currentThread().getName()+"的版本号"+stamp);
			try {
				// 线程t2拿到版本号后,交给线程t1执行ABA操作
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			boolean  res = asr.compareAndSet(100, 2019, stamp, stamp+1);
			System.out.print(Thread.currentThread().getName()+"\t");
			System.out.println("是否修改成功:"+res+"当前最新版本号:"+asr.getStamp()+"当前最新值:"+asr.getReference());
		},"t2").start();

打印结果:

当前线程t1的版本号1

当前线程t2的版本号1

t1 当前版本号2

t1 当前版本号3

t2 是否修改成功:false当前最新版本号:3当前最新值

结论:使用使用原子引用+版本号控制可以解决ABA问题,线程t1修改两次后,版本号变化,线程t2在对值修改的时候发现版本号不对,就不会对值进行修改

我的博客:添加链接描述

你可能感兴趣的:(java高并发编程)