理会 CAS 和 CAS:
有时候面试官面试问你的时候, 会问, 谈谈你对 CAS 的理解, 这时应该有很多人, 就会比较懵, 当然, 我也会比较懵, 当然我和很多人的懵不同, 很多人可能, 并不知道 CAS 是一个什么东西, 而在我看来我是不知道他问的是那个 CAS
我一般会问面试官, 问他问的 CAS 是 "原子操作", 还是 "单点登录"
因为在 JAVA 并发中的原子操作是称为 CAS 的, 也就是英文单词 CompareAndSwap 的缩写, 中文意思是: 比较并替换.
但是在企业应用中 CAS 也被称为企业级开源单点登录解决方案, 是 Central Authentication Service 的缩写 -- 中央认证服务, 一种独立开放指令协议, 是 Yale 大学发起的一个企业级开源项目, 旨在为 web 应用系统提供一种可靠的 SSO 解决方案.
CAS(Compare And Swap):
我们先要学习的是并发编程中的 CAS, 也就是原子操作
那么, 什么是原子操作? 如何实现原子操作?
什么是原子操作:
原子, 也是最小单位, 是一个不可再分割的单位, 不可被中断的一个或者一系列操作
CAS 是以一种无锁的方式实现并发控制, 在实际情况下, 同时操作一个对象的概率非常小, 所以多数加锁操作做的基本是无用功
CAS 以一种乐观锁的方式实现并发控制
如何实现原子操作:
Java 可以通过锁和循环 CAS 的方式实现原子操作
为什么要有 CAS:
CAS 就是比较并且替换的一个原子操作, 在 CPU 的指令级别上进行保证
为什么要有 CAS:
Sync 是基于阻塞的锁的机制,
1: 被阻塞的线程优先级很高
2: 拿到锁的线程一直不释放锁则么办
3: 大量的竞争, 消耗 CPU, 同时带来死锁或者其他线程安全
因为通过锁实现原子操作时, 其他线程必须等待已经获得锁的线程运行完车之后才能获取锁, 这样就会占用系统大量资源
CAS 原理:
从 CPU 指令级别保证这是一个原子操作
CAS 包含哪些参数:
三个运算符:
一个内存地址 V
一个期望的值 A
一个新值 B
基本思路:
如果地址 V 上的值和期望的值 A 相等, 就给地址 V 赋值新值 B, 如果不是, 不做任何操作
循环 CAS:
在一个 (死) 循环中 [for(;;)] 里不断进行 CAS 操作, 直到成功为止(自旋操作即死循环)
CAS 问题:
ABA 问题:
那么什么是 ABA 问题? 就是内存中原本是 A, 然后通过 CAS 变成了 B, 然后再次通过 CAS 变成了 A, 这个过程中, 相对于结果来说, 是没有任何改变的, 但是相对于内存来说, 至少发生过两次变化, 这就是 ABA 问题
生活中:
就像你接了一杯水, 这时水是满的, 但是这个时候, 你的同时很渴, 过来拿你的水直接喝掉了一半, 这时水剩下了一半, 接着, 你的同事又重新把你的水帮你接满了, 那么这时你的水还是满的, 相对于水来说, 他还是满的, 但是相对于杯子来说, 他已经被用过了两次, 一次是喝水, 一次是接水, 这就是 ABA 问题
从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题. 这个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用, 并且当前标志是否等于预期标志, 如果全部相等, 则以原子方式将该引用和该标志的值设置为给定的更新值.
生活中:
你接了一杯水, 然后旁边放上一张登记表, 这个时候你同事过来, 直接喝掉了一半, 然后登记上, XXX 喝掉了一半的水, 然后去给你接满了, 再次登记上, 我给你接满了, 这时, ABA 的问题就得到了解决, 你一看这个表就知道了一切
开销问题:
在自旋或者死循环中不断进行 CAS 操作, 但是长期操作不成功, CPU 不断的循环, 带来的开销问题
自旋 CAS 如果长时间不成功, 会给 CPU 带来非常大的执行开销. 如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升, pause 指令有两个作用, 第一它可以延迟流水线执行指令 (de-pipeline), 使 CPU 不会消耗过多的执行资源, 延迟的时间取决于具体实现的版本, 在一些处理器上延迟时间是零. 第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation) 而引起 CPU 流水线被清空(CPU pipeline flush), 从而提高 CPU 的执行效率.
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时, 我们可以使用循环 CAS 的方式来保证原子操作, 但是对多个共享变量操作时, 循环 CAS 就无法保证操作的原子性, 这个时候就可以用锁, 或者有一个取巧的办法, 就是把多个共享变量合并成一个共享变量来操作. 比如有两个共享变量 i=2,j=a, 合并一下 ij=2a, 然后用 CAS 来操作 ij. 从 Java1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性, 你可以把多个变量放在一个对象里来进行 CAS 操作.
CAS 的目的:
利用 CPU 的 CAS 指令, 同时借助 JNI 来完成 Java 的非阻塞算法. 其它原子操作都是利用类似的特性完成的. 而整个 J.U.C 都是建立在 CAS 之上的, 因此对于 synchronized 阻塞算法, J.U.C 在性能上有了很大的提升.
JDK 中相关原子操作类的使用:
更新基本类型类: AtomicBoolean,AtomicInteger,AtomicLong
更新数组类: AtomicIntegerArray,AtomicLongArray,AtomicReferenceArrat
更新引用类型: AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
理论已经理解的差不多了, 接下来写写代码
使用 AtomicInteger
- package org.dance.day3;
- import java.util.concurrent.atomic.AtomicInteger;
- /**
- * 使用原子类 int 类型
- * @author ZYGisComputer
- */
- public class UseAtomicInt {
- static AtomicInteger atomicInteger = new AtomicInteger(10);
- public static void main(String[] args) {
- // 10->11 10 先去再增加
- System.out.println(atomicInteger.getAndIncrement());
- // 11->12 12 先增加再取
- System.out.println(atomicInteger.incrementAndGet());
- // 获取
- System.out.println(atomicInteger.get());
- }
- }
返回值:
10 12 12
通过返回值可以看到, 第一个是先获取返回值后累加 1, 第二个是先累加 1 后再返回, 第三个是获取当前值
使用 AtomicIntegerArray
- package org.dance.day3;
- import java.util.concurrent.atomic.AtomicIntegerArray;
- /**
- * 使用原子类 int[]
- * @author ZYGisComputer
- */
- public class UseAtomicIntegerArray {
- static int[] values = new int[]{1,2};
- static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(values);
- public static void main(String[] args) {
- // 改变的第一个参数是 数组的下标, 第二个是新值
- atomicIntegerArray.getAndSet(0,3);
- // 获取原子数组类中的下标为 0 的值
- System.out.println(atomicIntegerArray.get(0));
- // 获取源数组中下标为 0 的值
- System.out.println(values[0]);
- }
- }
返回结果:
3 1
通过返回结果我们可以看到, 源数组中的值并没有改变, 只有引用中的值发生了改变, 这是则么回事?
- /**
- * Creates a new AtomicIntegerArray with the same length as, and
- * all elements copied from, the given array.
- *
- * @param array the array to copy elements from
- * @throws NullPointerException if array is null
- */
- public AtomicIntegerArray(int[] array) {
- // Visibility guaranteed by final field guarantees
- this.array = array.clone();
- }
通过看源码我们得知他是调用了数组的克隆方法, 克隆了一个一模一样的
使用 AtomicReference
- package org.dance.day3;
- import java.util.concurrent.atomic.AtomicReference;
- /**
- * 使用原子类引用类型
- * @author ZYGisComputer
- */
- public class UseAtomicReference {
- static AtomicReference<UserInfo> atomicReference = new AtomicReference<>();
- public static void main(String[] args) {
- UserInfo src = new UserInfo("彼岸舞",18);
- // 使用原子引用类包装一下
- atomicReference.set(src);
- UserInfo target = new UserInfo("彼岸花",19);
- // 这里就是 CAS 改变了, 这个应用类就好像一个容器也就是内存 V, 而 src 就是原值 A,target 就是新值 B
- // 期望原值是 src, 如果是的话, 改变为 target, 否则不变
- atomicReference.compareAndSet(src,target);
- System.out.println(atomicReference.get());
- System.out.println(src);
- }
- static class UserInfo{
- private String name;
- private int age;
- @Override
- public String toString() {
- return "UserInfo{" +
- "name='" + name + '\'' +
- ", age=" + age +
- '}';
- }
- public UserInfo() {
- }
- public UserInfo(String name, int age) {
- this.name = name;
- this.age = age;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public int getAge() {
- return age;
- }
- public void setAge(int age) {
- this.age = age;
- }
- }
- }
返回结果:
- UserInfo{
- name='彼岸花', age=19
- }
- UserInfo{
- name='彼岸舞', age=18
- }
通过返回结果可以直观的看到, 原子引用类中的值发生了改变, 但是源对象 src 却没有改变, 因为原子引用类和原对象本身是两个东西, CAS 后就可以理解为内存中的东西变了, 也可以说是引用变了, 他只能保证你在改变这个引用的时候保证是原子性的
记得之前上面说的 ABA 问题吧, 在这里就是解决代码
JDK 中提供了两种解决 ABA 问题的类
AtomicStampedReference
AtomicStampedReference, 里面是用 int 类型, 他关心的是被人动过几次
AtomicMarkableReference
AtomicMarkableReference, 里面是用 boolean 类型, 他只关心这个版本有没有人动过
两个类关心的点不一样, 侧重的方向不一样, 就像之前说的喝水问题, AtomicStampedReference 关心的是, 被几个人动过, 而 AtomicMarkableReference 关心的是有没有人动过
使用 AtomicStampedReference 解决 ABA 问题
- package org.dance.day3;
- import java.util.concurrent.atomic.AtomicStampedReference;
- /**
- * 使用版本号解决 ABA 问题
- * @author ZYGisComputer
- */
- public class UseAtomicStampedReference {
- /**
- * 构造参数地第一个是默认值, 第二个就是版本号
- */
- static AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("src",0);
- public static void main(String[] args) throws InterruptedException {
- // 获取初始版本号
- final int oldStamp = atomicStampedReference.getStamp();
- // 获取初始值
- final String oldValue = atomicStampedReference.getReference();
- System.out.println("oldValue:"+oldValue+"oldStamp:"+oldStamp);
- Thread success = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println(Thread.currentThread().getName()+", 当前变量值:"+oldValue+"当前版本号:"+oldStamp);
- // 变更值和版本号
- /**
- * 第一个参数: 期望值
- * 第二个参数: 新值
- * 第三个参数: 期望版本号
- * 第四个参数: 新版本号
- */
- boolean b = atomicStampedReference.compareAndSet(oldValue, oldValue + "java", oldStamp, oldStamp + 1);
- System.out.println(b);
- }
- });
- Thread error = new Thread(new Runnable() {
- @Override
- public void run() {
- // 获取原值
- String sz = atomicStampedReference.getReference();
- int stamp = atomicStampedReference.getStamp();
- System.out.println(Thread.currentThread().getName()+", 当前变量值:"+sz+"当前版本号:"+stamp);
- boolean b = atomicStampedReference.compareAndSet(oldValue, oldValue + "C", oldStamp, oldStamp + 1);
- System.out.println(b);
- }
- });
- success.start();
- success.join();
- error.start();
- error.join();
- System.out.println(atomicStampedReference.getReference()+":"+atomicStampedReference.getStamp());
- }
- }
返回结果:
oldValue:src oldStamp:0
Thread-0, 当前变量值: src 当前版本号: 0
true
Thread-1, 当前变量值: srcjava 当前版本号: 1
false
srcjava:1
通过返回结果可以观察到, 原始值是 src, 版本是 0, 然后使用 join 方法使我们的正确线程确保咋错误线程之前执行完毕, 当正确线程执行完毕后, 会把值改为 srcjava, 版本改为 + 1, 然后执行错误的线程, 错误的线程在尝试去改值的时候, 发现期望的值是 src, 但是值已经被改变成 srcjava 了, 并且期望的版本是 0, 但是版本已经被改为 1 了, 所以他无法修改, 在两个线程都执行完毕之后, 打印的值是 srcjava, 版本是 1, 成功的解决了 ABA 问题, 当然在这里面我的期望值是还是 src, 也可以改为 src+java 但是因为版本不一样也是无法修改成功的; 亲测没问题
原子更新字段类就不写了, 那个使用比较麻烦, 如果多个字段的话, 就直接使用 AtomicReference 类就可以了
来源: https://www.cnblogs.com/flower-dance/p/13767956.html