前言:
如果不知道JMM(java 内存模型),还请看博主的上一篇博客:
volatile关键字与内存可见性详解
,因为本博客还需要JMM的理论基础。
博客内容导图
1、CAS是什么
CAS(Compare-And-Swap)是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问.
CAS 是一种无锁的非阻塞算法的实现。
以上是一本书给出的定义。其实我们在上篇博客已经提过CAS了,AtomicInteger 实现num++,AtomicInteger的底层就是用了CAS的思想。
CAS是一条CPU并发原语.并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条原子指令,不会造成所谓的数据不一致(线程安全)的问题.
2、CAS解决了什么问题
对于多线程而言,实现线程安全是最要的也是最基本的,我们之前通过同步锁synchronized()实现,它保证了线程安全,也就是数据的一致性,但是它的并发性下降,因为每次在都要对代码块进行加锁,线程得到锁才可以运行。但是CAS是一种无锁的非阻塞的算法实现,如果线程数不多(并发量小),他的性能要比synchronized()高的多,但是线程数过多,就会过分的消耗cpu资源(具体为啥后面会解释)。
3、CAS的原理
CAS 包含了 3 个操作数:
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操作
。接下来用源码来验证以上理论。
3.1 atomic包中的类
且看AtomicInteger,AtomicInteger位于java.util.concurrent.atomic包下,如下图:(本图截取自JDK API 1.6中文版)
从上图可知道,atomic包下都是原子性的操作,原子性就是说无论是几行代码,要么这些代码一块执行,要么就不执行,中间不可以 加塞(比如还没执行完,发生线程调度),这就完美的解决num++线程不安全的问题。
3.2 根据atomicInteger.getAndIncrement()源码验证CAS理论
(1)从下面的代码可以看出,用到了Unsafe这个类,
UnSafe类在sun.misc包中
,UnSafe是CAS的核心类 ,由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,
基于该类可以直接操作特额定的内存数据,其内部方法操作可以像C的指针一样直接操作内存。
UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务
看到这里的value是用关键字
volatile修饰的
,保证了内存可见性
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 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;
(2)如下面的代码:
unsafe.getAndAddInt(this, valueOffset, 1),传了三个参数,分别是当前对象this、该对象在内存中的偏移地址,还有表示每次都加1的数字1。其中UnSafe就是根据内存偏移地址获取数据的偏移地址
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
(3)如下代码,进入到方法getAndAddInt,发现是一个do while 循环,接下来进行说明:
var1 AtomicInteger对象本身
var2 该对象值的引用地址
var4 需要变动的数量
var5 是通过var1 var2找出内存中真实的值
this.compareAndSwapInt(var1, var2, var5, var5 + var4),表示的是该对象var1在内存中的偏移量var2位置的值与期望值var5进行比较,如果相同,更新var5+var4的值并且返回true,如果不同,继续循环,将期望值var5改为内存中真正的值,然后在进行比较,直到更新完成。
综上也体现出了它的缺点,就是如果线程过多,其他的线程可能会一直改内存中的实际值,那么期望值var5就会很难和内存中的实际值相等,这样就会导致一直循环,效率下降,所以如果线程多,并发大,用锁比较合适。
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;
}
(4)CAS原理分析
1.假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上)且AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存.
2.线程A通过getIntVolatile(var1,var2) 拿到value值3,这时线程A被挂起.线程B也通过getIntVolatile(var1,var2) 拿到value值3,
3.此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存中的值也是3 ,成功修改内存的值为4 线程B走完收工 一切OK。
4.这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数值4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了.
5.线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功.
4、CAS的缺点
(1)根据上面所说的原理,如果并发量特别大或者说开启的线程数过多,就可能会存在内存中的共享的值经常被修改,导致unsafe底层匹配不成功,会一直do-while下去,加大了cpu 的压力
(2)只能保证一个共享变量的原子操作。(比如声明两个原子变量a和b 具体操作是a+b,此时只能枷锁了)
(3)引发ABA问题(狸猫换太子)
ABA问题举例:
场景1:
一个线程1从内存中取出A,这个时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,线程1此时还被阻塞,线程2又进行了一些操作,然后将B又变成了A,此时线程1获得资源,开始执行,但是在进行cas操作的时候发现内存中还是A,然后线程1执行成功。(
说白了就是可能存在一个线程根本不知道数值发生了变化
)
场景2:
ABA问题:内存值V=100;
threadA 将100,改为50;
threadB 将100,改为50;
threadC 将50,改为100;
小牛取款,由于机器不太好使,多点了几次取款操作。后台threadA和threadB工作,此时threadA操作成功(100->50),threadB,取完值100,然后阻塞。正好牛妈打款50元给小牛(50->100),threadC执行成功,之后threadB运行了,又改为(100->50)。
牛气冲天,lz钱哪去了???
5、如何解决ABA问题:
对内存中的值加个版本号,在比较的时候除了比较值还的比较版本号。
AtomicStampedReference就是用版本号实现cas机制,再讲AtomicStampedReference之前,先讲解AtomicReference原子引用。
5.1 AtomicReference原子引用
AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。我们可以自定义对象。
代码如下:
public class TestAtomic {
public static void main(String[] args) {
AtomicReference<User> atomicUser=new AtomicReference<>();
User zhangsan=new User("张三",21);
User lisi =new User("李四",25);
atomicUser.set(zhangsan);
System.out.println(atomicUser.compareAndSet(zhangsan,lisi)+"\t"+atomicUser.get().toString());
}
}
class User{
private String username;
private int age;
public User(String username,int age){
this.username=username;
this.age=age;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
执行结果:
// 分析:
定义了对象User,利用方法compareAndSet进行比较和判断,内存中存入了张三,然后和compareAndset的第一个参数进行比较,发现都是张三,然后修改为lisi。
5.2 版本号原子引用 AtomicStampedReference解决ABA 问题
AtomicStampedReference 增加了版本号,解决了ABA问题。
思想:
内存值V=100;
threadA 获取值100 版本号1,改为50 版本号2;
threadB 获取值100 版本号1
threadC 将50 版本号2 改为100 版本号3
场景:小牛取款,由于机器不太好使,多点了几次取款操作。后台threadA和threadB工作,此时threadA操作成功(100->50) 版本号1->版本号2,threadB取完值100,版本号是1,然后阻塞。正好牛妈打款50元给小牛(50->100) 版本号2->版本号3,threadC执行成功,之后threadB获取资源,开始执行,发现内存的值为100, 但是版本号不对应, 所以重新从内存中读取值,然后再进行判断。这次牛就不会冲天了,哈哈哈哈哈!
以上场景对应代码:
public class ABADemo {
// 表示内存里初始值是100,版本好号为1
private static AtomicStampedReference atomicStampedReference=new AtomicStampedReference(100,1);
public static void main(String[] args) {
new Thread(()->{
// 获得版本号
int stamp = atomicStampedReference.getStamp();
// 这里延迟一秒不是为了说为了线程阻塞,而是为了让线程B得到版本号
System.out.println(Thread.currentThread().getName()+"\t第一次版本号:"+stamp);
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
// 期望值为100 和内存中的值进行比较,如果一样,且版本号stamp也和内存中一样,则改为50
atomicStampedReference.compareAndSet(100,50,stamp,stamp+1);
System.out.println(Thread.currentThread().getName()+"\t第二次版本号:"+atomicStampedReference.getStamp());
// atomicStampedReference.compareAndSet(100,50,stamp,stamp+1);
},"A").start();
new Thread(()->{
// 获得版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第一次版本号:"+stamp);
// 线程阻塞
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
// 期望值为100 和内存中的值进行比较,如果一样,且版本号stamp也和内存中一样,则改为50
boolean result = atomicStampedReference.compareAndSet(100, 50, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName()+"\t修改是否成功"+result+"\t当前实际版本号:"+atomicStampedReference.getStamp());
System.out.println("当前实际最新值:"+atomicStampedReference.getReference());
},"B").start();
new Thread(()->{
// 这里延迟两秒不是为了说为了线程阻塞,而是为了让线程A 执行完毕
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
// 获得版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第一次版本号:"+stamp);
// 期望值为100 和内存中的值进行比较,如果一样,且版本号stamp也和内存中一样,则改为50
atomicStampedReference.compareAndSet(50,100,stamp,stamp+1);
System.out.println(Thread.currentThread().getName()+"\t第二次版本号:"+atomicStampedReference.getStamp());
},"C").start();
}
}
运行结果:
后记:
总结不易,如果对你有帮助,请点个赞欧!
·