java并发 - 学习CAS
学习CAS
一丶什么是CAS
CAS(Compare And Swap) 即比较交换, 给定一个期望值, 当将要修改的变量值和期望值一致时(即其他线程没有修改), 就修改对应的变量值, 否则什么也不做, 它允许再次尝试比较交换, 直到成功为止.
二丶CAS算法过程
CAS(V,E,N). V表示要更新的变量, E表示预期值, N表示将要更新的值.
仅当V=E时, 才会将V更新为N , 如果 V ≠ E, 说明有其他线程更新了V, 当前线程什么也不用做. 最后CAS返回当前V的真实值
三丶悲观锁与乐观锁
对于并发控制而言, 锁是一种悲观策略, 它总是认为多线程操作有可能冲突, 因此总要加锁控制,阻塞等待.
无锁则是一种乐观策略, 它总是认为多线程操作很少会冲突, 无须加锁, 如果有冲突, 再次重新尝试操作直到没有冲突即可.
无锁主要是使用CAS实现的, 它是一种"乐观锁"
无锁的优点:
1. 不会产生死锁的问题
2. 没有锁竞争带来的系统开销
3. 没有线程间的频繁调度带来的开销
4. 性能更优越
四丶CPU指令支持
在硬件层面, 大部分的现代处理器都已经支持原子化的CAS指令.在JDK5.0以后, 虚拟机便可以使用这个指令来实现并发操作和并发数据结构, 并且, 这种操作在虚拟机中可以说是无处不在.
五丶unsafe类
源码入口 AtomicInteger的compareAndSet()方法 (JDK 8)
/** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that * the actual value was not equal to the expected value. */ public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
unsafe是一个不安全类, jdk不允许开发者直接使用.它的内部方法可以像指针一样直接操作内存. 资料 -- <java并发变成-无锁CAS>
unsafe的compareAndSwapInt()方法, 内部使用CAS原子指令来完成
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
第一个参数o为给定的对象, offset为对象内的偏移量(通过偏移量,可以快速定位获取对应的字段变量), expected表示期望值, x表示要设置的值.如果指定的字段的值等于expected, 那么就会把它设置为x
六丶Atomic原子类
Atomic原子类都是使用CAS操作来实现线程安全操作的, 底层调用unsafe类方法, unsafe则使用了CAS原子指令
以AtomicInteger为例, 内部记录维护一个使用volatile关键字修饰的int变量, (volatitle只保证线程间的可见性,即一个线程修改该变量, 另一个线程能够知道, 但它并不保证原子性)
AtomicInteger的线程安全自增方法
/** * Atomically increments by one the current value. * * @return the updated value */ public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
unsafe中的getAndAddInt()方法
public final int getAndAddInt(Object o, long offset, int delta) { int curr; do { curr = this.getIntVolatile(o, offset); // 由于使用了volatile, 修改操作能够被各线程感知 } while(!this.compareAndSwapInt(o, offset, curr, curr + delta)); //如果cas操作没有成功, 则继续尝试, compareAndSwapInt()方法见第五小节解释 return curr; }
七丶ABA问题
假设一个变量v初始值为0, t1,t2线程读取的值都为0, 由于线程调度等其他问题, t1并未继续cas操作, 期间t2使用cas操作, 将v加1变成1, 线程t3使用cas操作将v减1变成0, 之后t1获得cpu继续执行, t1使用cas操作时, 发现v值仍为0, 以为v没有变化, 实际已发生变化, 这样在某些场景就会出现问题, 这就是ABA问题
使用带有时间戳的对象引用AtomicStampedReference可以有效解决该问题, AtomicStampedReference内部除了维护对象值, 还维护了一个时间戳(实际可以是任何一个整数标识来表示状态值, 也可以是版本号). AtomicStampedReference在更新对象值之外,还必须更新时间戳,在更新对象值前, 除了比较对象值之外,还必须比较时间戳, 只有这两个条件都相等的情况下,才会去更新值,否则认为被其他线程修改
八丶示例
public class AddCASRunnable implements Runnable { private AtomicInteger data =new AtomicInteger(0); public int getResult(){ return data.get(); } @Override public void run() { for(int i=0;i < 10000000; i++){ data.incrementAndGet(); } } }
public class AddUnsafeRunnable implements Runnable { private int data=0; public int getResult(){ return data; } @Override public void run() { for(int i=0;i < 10000000; i++){ data++; } } }
public class Client { public static void main(String[] args) throws InterruptedException { AddCASRunnable addCASRunnable=new AddCASRunnable(); Thread t1=new Thread(addCASRunnable); Thread t2=new Thread(addCASRunnable); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("CAS 结果为: "+addCASRunnable.getResult()); // CAS 结果为: 20000000 AddUnsafeRunnable addUnsafeRunnable=new AddUnsafeRunnable(); Thread t3=new Thread(addUnsafeRunnable); Thread t4=new Thread(addUnsafeRunnable); t3.start(); t4.start(); t3.join(); t4.join(); System.out.println("unsafe 结果为: "+addUnsafeRunnable.getResult()); // unsafe 结果为: 19994931 } }
源码地址为: https://gitee.com/timfruit189/java-learning/tree/master/src/main/java/com/ttx/java/concurrent/cas
学习资料:
<java高并发程序设计> 葛一鸣 郭超 编著