多线程(五)CAS

12 篇文章 0 订阅

简单的实现CAS

需求有100个线程同时访问,并且每个线程发起10次请求,最后count次数应该是1000次。

public class Demo {
	//总访问量。volatile保证多线程之间count变量的可见性
	private volatile static int count = 0;

	/**
	 * count ++ 操作实际上是由3步来完成!(jvm执行引擎)
	 *    1.获取count的值,记做A : A=count
	 *    2.将A值+1,得到B :B=A+1
	 *    3.将B值赋值给count
	 *    修改升级第3步的实现:(compareAndSwap方法)
	 *       1.获取锁
	 *       2.获取count最新的值,记做LV
	 *       3.判断LV是否等于A,如果相等,则将B的值赋值给count,并返回true,否则返回false
	 *       4.释放锁
	 */
	//模拟访问的方法
	public static void request() throws InterruptedException {
		//模拟耗时5ms
		TimeUnit.MILLISECONDS.sleep(5);

		//表示期望值
		int expectCount;
		while (!compareAndSwap((expectCount = getCount()), expectCount + 1)) {}
	}

	/**
	 * @param expectCount 期望值count
	 * @param newCount 需要给count赋值的新值
	 * @return count当前值和期望值expectCount一致返回true
	 */
	public static synchronized boolean compareAndSwap(int expectCount, int newCount) {
		//判断count当前值是否和期望值expectCount一致,如果一致,将newCount赋值给count
		if (getCount() == expectCount) {
			count = newCount;
			return true;
		}
		return false;
	}

	public static int getCount() {return count;}

	public static void main(String[] args) throws InterruptedException {
		//开始时间
		long startTime = System.currentTimeMillis();
		int threadSize = 100;
		CountDownLatch countDownLatch = new CountDownLatch(threadSize);

		for (int i = 0; i < threadSize; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						for (int j = 0; j < 10; j++) {
							request();
						}
					} catch (InterruptedException e) {
						e.printStackTrace();
					} finally {
						countDownLatch.countDown();
					}
				}
			}).start();
		}
		countDownLatch.await();
		long endTime = System.currentTimeMillis();

		System.out.println(Thread.currentThread().getName() + "耗时:" + (endTime - startTime) + ",count = " + count);

	}
}

在这里插入图片描述

JDK CAS支持

  • CAS 全称“CompareAndSwap”,中文翻译过来为“比较并替换”
    定义:
    • CAS操作包含三个操作数————内存位置(V)、期望值(A)和新值(B)。
    • 如果内存位置的值与期望值匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不作任何操作。
    • 无论哪种情况,它都会在CAS指令之前返回该位置的值。(CAS在一些特殊情况下仅返回CAS是否成功,而不提取当前值)
  • CAS有效的说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置的值,只告诉我这个位置现在的值即可。”

怎么使用JDK提供的CAS支持?

  • java中提供了对CAS操作的支持,具体在sun.misc.unsafe类中,声明如下:
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

    参数var1:表示要操作的对象
    参数var2:表示要操作对象中属性地址的偏移量
    参数var4:表示需要修改数据的期望的值
    参数var5:表示需要修改为的新值

CAS实现原理是什么?

  • CAS通过调用JNI的代码实现,JNI:java Native Interface,允许java调用其它语言。
    而compareAndSwapxxx系列的方法就是借助“C语言”来调用cpu底层指令实现的。
    以常用的Intel x86平台来说,最终映射到的cpu的指令为“cmpxchg”,这是一个原子指令,cpu执行此命令时,实现比较并替换的操作!

CAS源码

两个关键点:

  • 自旋;
  • unsafe类。

当点开compareAndSet方法后:

// AtomicInteger类内部
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
  • 通过这个方法,我们可以找出AtomicInteger内部维护了volatile int value和private static final Unsafe unsafe两个比较重要的参数。(注意value是用volatile修饰)
  • 还有变量private static final long valueOffset,表示该变量在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
  • 变量value用volatile修饰,保证了多线程之间的内存可见性。
// AtomicInteger类内部
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;
  • 然后我们通过compareAndSwapInt找到了unsafe类核心方法:
//unsafe内部类
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;
}
  • AtomicInteger.compareAndSwapInt()调用了Unsafe.compareAndSwapInt()方法。Unsafe类的大部分方法都是native的,用来像C语言一样从底层操作内存。

  • 这个方法的var1和var2,就是根据对象和偏移量得到在主内存的快照值var5。然后compareAndSwapInt方法通过var1和var2得到当前主内存的实际值。如果这个实际值跟快照值相等,那么就更新主内存的值为var5+var4。如果不等,那么就一直循环,一直获取快照,一直对比,直到实际值和快照值相等为止。

  • 比如有A、B两个线程

一开始都从主内存中拷贝了原值为3;
1、A线程执行到var5=this.getIntVolatile,即var5=3。此时A线程挂起;

2、B修改原值为4,B线程执行完毕,由于加了volatile,所以这个修改是立即可见的;

3、A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存的值不等于快照值3,所以继续循环,重新从主内存获取。

4、线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直至成功。

现代计算机动不动就上百核心,cmpxchg怎么保证多核心下的线程安全?

  • 系统底层进行CAS操作的时候,会判断当前系统是否为多核心系统,如果是就给“总线”加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作,也就是说CAS的原子性是平台级别的!

什么是ABA问题?

  • CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,在CAS方法执行之前,被其它线程修改为了B、然后又修改回了A,那么CAS方法执行检查的时候会发现它的值没有发生变化,但是实际却变化了。这就是CAS的ABA问题。

如何解决ABA问题?

  • 解决ABA最简单的方案就是给值加一个修改版本号,每次值变化,都会修改它的版本号,CAS操作时都去对比此版本号。
    java中ABA解决方法(AtomicStampedReference)
    AtomicStampedReference主要包含一个对象引用及一个可以自动更新的整数“stamp”的pair对象来解决ABA问题。

使用AtomicStampedReference修改ABAbug

public class CAS_ABA {
	//参数:初始值和初始版本号。  泛型:此引用的对象的类型
	public static AtomicStampedReference<Integer> a = new AtomicStampedReference(new Integer(1), 1);

	public static void main(String[] args) {
		new Thread(new Runnable(){
			@Override
			public void run() {
				System.out.println("操作线程:" + Thread.currentThread().getName() + ",初始值:" + a.getReference());
				try {
					//public V getReference()返回引用的当前值。
					Integer expectReference = a.getReference();
					Integer newReference = expectReference + 1;
					//public int getStamp()返回stamp(类似版本号)的当前值。
					Integer stamp = a.getStamp();
					Integer newStamp = stamp + 1;
					//主线程休眠1s,让出CPU
					Thread.sleep(1000);
					/*
					public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
					如果expectedReference==newReference,且expectedStamp==newStamp,则将该值原子设置为给定的更新值。
					参数:
						expectedReference - 参考的预期值
						newReference - 参考的新值
						expectedStamp - Stamp的预期值
						newStamp - Stamp新值 */
					boolean isCASSucceed = a.compareAndSet(expectReference, newReference, stamp, newStamp);//若输出true,则有ABA问题
					System.out.println("操作线程:" + Thread.currentThread().getName() + ",CAS操作:" + isCASSucceed + ",a的值为:" + a.getReference());
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "MAIN").start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					//先休眠,确保Thread-MAIN线程优先执行
					Thread.sleep(20);
					a.compareAndSet(a.getReference(), a.getReference()+1, a.getStamp(), a.getStamp()+1);
					System.out.println("操作线程:" + Thread.currentThread().getName() + ",【increment】值:" + a.getReference());
					a.compareAndSet(a.getReference(), a.getReference()-1, a.getStamp(), a.getStamp()+1);
					System.out.println("操作线程:" + Thread.currentThread().getName() + ",【decrement】值:" + a.getReference());
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "干扰线程").start();
	}
}

CAS总结

任何技术都不是完美的,当然,CAS也有他的缺点:

CAS实际上是一种自旋锁,

  • 一直循环,开销比较大。

  • 只能保证一个变量的原子操作,多个变量依然要加锁。

  • 引出了ABA问题(AtomicStampedReference可解决)。

而他的使用场景适合在一些并发量不高、线程竞争较少的情况,加锁太重。但是一旦线程冲突严重的情况下,循环时间太长,为给CPU带来很大的开销。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值