多线程-CAS

什么是CAS操作

  • 通俗来讲就是比较并交换(Compare And Swap)。
  • 是一条CPU并发原语
  • 他是判断内存某个位置的值是否为预期值,如果是则更改为新值,否则线程不挂起,持续比较到主内存工作内存中的值一致为止。
  • 整个执行过程都是满足原子性的。

为什么不会造成数据不一致

  • CAS操作体现在Java语言之中就是 Unsafe类 中的各个方法。

  • 调用 Unsafe类 中的CAS操作,JVM 会帮我们实现出 CAS的汇编指令

  • 这是一种完全依赖于硬件的功能,通过它实现原子操作

  • 实现逻辑上来说就是:

    • CAS系统原语
    • 原语属于操作系统用语范畴。
    • 若干条操作系统指令构成,用于完成某个功能的一个过程。
    • 原语的执行必须是连续的,执行过程中不能被中断。
    • CAS指令是一条CPU原子指令。
    • 所以不会导致数据不一致问题。

案例演示

演示用到的关键方法

  • compareAndSet 方法:
  • 参数一:期望值,主存中的值。
  • 参数二:更新值。
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

单线程中

package JUC;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author zhaolimin
 * @date 2021/11/13
 * @apiNote CAS测试
 */
public class CASDemo01 {

    public static void main(String[] args) {
        

        AtomicInteger atomicInteger = new AtomicInteger(10);

        System.out.println(atomicInteger.compareAndSet(10, 2021) + "\t 当前atomicInteger在主内存中的值为:" + atomicInteger.get());
        // 第三条语句,已经 atomicInteger 被修改为 2021 了与期望值 10 不一样,所以修改不了。
        System.out.println(atomicInteger.compareAndSet(10, 2022) + "\t 当前atomicInteger在主内存中的值为:" + atomicInteger.get());
    }
}

  • 1、此时主内存中,atomicInteger 的值为10
  • 2、我们使用 CAS操作 开始准备对主存中 atomicInteger 的值的更改。
    • 2.1、compareAndSet方法第一个参数为 expect 期望值,我们期望主存中的 atomicInteger 的值为10的时候更改,所以这里第一个参数传入10
    • 2.2、第二个参数是想要更改的值,我们此时将 2021 设置为 atomicInteger 更改后的值
    • 2.3、如果此时我们传入的期望 atomicInteger 的值与主内存中 atomicInteger 的值一样,就进行 10 -> 2021 的更改,否则就自旋不做任何操作。

多线程中

package JUC;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author zhaolimin
 * @date 2021/11/13
 * @apiNote CAS测试
 */
public class CASDemo01 {

    public static void main(String[] args) {

        AtomictoInteger atomicInteger = new AtomicInteger(10);

        //System.out.println(atomicInteger.compareAndSet(10, 2021) + "\t 当前atomicInteger在主内存中的值为:" + atomicInteger.get());
        //System.out.println(atomicInteger.compareAndSet(10, 2022) + "\t 当前atomicInteger在主内存中的值为:" + atomicInteger.get());

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " " + atomicInteger.compareAndSet(10, 2021) + "\t 当前atomicInteger在主内存中的值为:" + atomicInteger.get());
            }, String.valueOf(i)).start();
        }

        System.out.println(Thread.currentThread().getName() + " " + atomicInteger.compareAndSet(10, 2022) + "\t 当前atomicInteger在主内存中的值为:" + atomicInteger.get());
    }
}
  • 和单线程的过程一样。

Unsafe类

这个类是什么

  • 存在于 rt.jar\sun\misc\Unsafe.class
  • CAS操作的核心类。
  • Java方法没有办法直接访问底层系统,需要通过本地原生方法来访问,Unsafe 类相当于一个后门,基于该类可以直接操作特定内存的数据,就像C/C++指针一样直接操作内存Java中的CAS操作依赖于Unsafe类的方法。
  • 该类中所有方法都是原生方法,换句话说就是该类中的方法都是直接调用操作系统底层资源执行相应的任务。
  • 该类中所有方法的执行都使用了汇编指令汇编指令的执行是原子的,可以保证操作的原子性

它是怎么操作内存中的对象的

  • valueOffset 要操作对象的内存地址偏移量来操作。

用AtomictoInteger类来说明

该类的重要属性以及静态代码块
// 获得 Unsafe 类的对象
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 内存地址偏移量,当前对象在主内存中的内存偏移量
private static final long valueOffset;
// 该类存储的值
private volatile int value;

static {
    try {
        // 得到当前对象在主内存中的地址偏移量
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}
我们先来看看这个类中的getAndIncrement方法的调用(重点)
public final int getAndIncrement() {
    /*
    	this:当前对象在内存中的起始地址
    	valueOffset:内存地址偏移量,当前对象在主内存中的内存偏移量
    	i:操作值
    */
    // 这个this和valueOffset理解可能有点困难,简单来讲就是,
    // this是指当前原子类对象,valueoffset 是指原子类对象里,变量 value 在 this内存区块 中的地址偏移量
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(java.lang.Object o, long l, int i) { /* compiled code */ } 
/**
	var1:当前对象,起始地址
	var2:当前对象在主内存中的内存偏移量
	var4:操作值
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // var5 就是当前this所指对象在内存中的地址加上当前对象内存偏移量在内存中存值的实际地址的值
        var5 = this.getIntVolatile(var1, var2); 
        // 不是期望值就会一直循环,等待 var5 与预期值对应上再修改,修改完毕再跳出。
        // 是期望值就接着+var4,直到最外面的循环结束
    } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}
// 先想办法获取变量 value 在内存中的地址。
// 通过 (Atomic::cmpxchg(x,addr,e)) 方法实现比较替换。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env,jobject unsafe,jobject objm jlong offset,jint e,jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt")  ;
opp p = JNIHandles::resolve(obj);
jint* addr = (jint*) index_opp_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x,addr,e)) == e;
UNSAFE_END
思考
  • 为什么不用 synchronized关键字 而是 CAS操作
    • synchronized 需要切换线程的状态,并且需要信号量监视器
    • synchronized 同一时间段只允许一个线程来访问,一致性得到了保证,但是并发能力下降
    • CAS操作 没有上述难题,但是很费CPU资源
  • 如果只是操作原子,只是单核中不可被打断,但是多核可以同时进行,A线程一个核修改B线程另一个核同时修改怎么办?
    • 因为其实在调用这个原子性的CPU指令compareAndSwap之前会判断是否是多核状态,是多核就再上一个总线锁多核CPU就只能有一个核能通过总线访问内存,这一个核执行完才能另一个核

CAS的缺点

循环时间长开销大

  • CAS操作由于线程不会被挂起CAS失败的话会一直自旋等待尝试,所以高并发下如果多个线程都在自旋等待,那么会很耗CPU资源

只能保证一个共享变量的原子操作

  • 对于多个共享变量的操作,CAS无法保证操作的原子性
  • 这个时候就可以用来协同保证操作的原子性

引出ABA问题

ABA问题

什么是ABA问题

  • CAS乐观锁操作实现的一个重要前提就是需要取出内存中某时刻的数据并在当下时刻比较并替换,那么再这个时间差之间会导致数据的变化。

如何产生(重点)

  • 假设有三个线程共享资源的值为B
  • 线程A、B、C分别拷贝了一份到了自己的工作内存中。
  • 线程A的期望值是B线程B的期望值是B线程C的期望值是A
  • 原子操作运行世间:B>C>A
  • 线程B此时先将共享资源的值设置为了A并写回主存,与线程C预期值正好相同为A
  • 线程C又将A改为B写回主存。
  • 由于线程阻塞的原因,可见性指每次读取值的时候可见,这里线程A第二次还没有读取值,也就是一次操作之间虽然是原子操作,但是操作的时间有长有短,线程A是最长的,在这次多核操作中,线程A的原子操作还没有完成,线程B和C的就已经完成了。还没到可见性的地步。
  • 由于以上原因,此时线程A它依旧以为主存中的值与它原子操作进行时的期望值一样,可实际上已经被线程B线程C操作过了。
  • 所以此时尽管线程ACAS操作时成功的,但是不代表这个过程就是正确的。
package JUC;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author zhaolimin
 * @date 2021/11/13
 * @apiNote ABA问题的解决
 */
public class ABADemo {

    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

    public static void main(String[] args) throws InterruptedException {

        // 假的ABA模拟
        // 暂停线程来模拟线程原子操作速度不一致问题
        // 例子不太对,线程 T3 应该在T1、T2修改之前就读到了100
        new Thread(() -> {
            System.out.println(atomicReference.compareAndSet(100, 101) + "\t" + Thread.currentThread().getName() + "\t" + atomicReference.get());
        }, "T1").start();

        // t2 线程暂停一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {

            System.out.println(atomicReference.compareAndSet(101, 100) + "\t" + Thread.currentThread().getName() + "\t" + atomicReference.get());
        }, "T2").start();

        // t3 线程暂停三秒
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {

            System.out.println(atomicReference.compareAndSet(100, 2000) + "\t" + Thread.currentThread().getName() + "\t" + atomicReference.get());
        }, "T3").start();
    }
}
/*
    true	T1	101
    true	T2	100
    true	T3	2000
*/
  • 为什么如果改成128或者**-129** 再用这个128或者**-129**作期望值会CAS失败?
    • Integer常量池默认是**-128 ~ 127**,AtomicReference对于Integer超过128会创建一个新的对象不使用缓存所以导致数据无法添加成功 。
    • 所以导致主物理内存中的Integer值工作内存中的Integer值不一致。

解决ABA问题

  • 原子引用 + 原子引用版本号(类似于时间戳)。
// T1	100 1 		    200 2	200 3
// T2	100 1	200 2	100 3
// 版本号不一致,以最新的为准,T1回滚重读主内存再更改

AtomicReference原子引用

  • 为了给引用类型变量也提供CAS操作,我们JUC提供了原子引用包装类
package JUC;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @author zhaolimin
 * @date 2021/11/13
 * @apiNote 源自引用测试。
 */

class User {

    String username;
    int age;

    public User(String username, int age) {
        this.username = username;
        this.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;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", age=" + age +
                '}';
    }
}

public class AtomicReferenceDemo {

    public static void main(String[] args) {

        User zlm = new User("zlm", 21);
        User wl = new User("wl", 20);

        AtomicReference<User> userAtomicReference = new AtomicReference<User>();
        userAtomicReference.set(zlm); // 主内存中的共享变量

        System.out.println(userAtomicReference.compareAndSet(zlm, wl) + "\t" + userAtomicReference.get().toString());
        System.out.println(userAtomicReference.compareAndSet(zlm, wl) + "\t" + userAtomicReference.get().toString());

    }
}

AtomicStampedReference版本号原子引用

  • 解决了ABA问题。
package JUC;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author zhaolimin
 * @date 2021/11/13
 * @apiNote ABA问题的解决
 */
public class ABADemo {

    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);

    public static void main(String[] args) throws InterruptedException {

        // 假的ABA模拟
        // 暂停线程来模拟线程原子操作速度不一致问题。
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);

            // t1 线程暂停一秒,确保 t2 线程能够拿到最初的版本号
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(atomicStampedReference.compareAndSet(100, 101,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1) + "\t" + Thread.currentThread().getName() + "\t");
            System.out.println(Thread.currentThread().getName() + "\t当前版本号:" + atomicStampedReference.getStamp());

            System.out.println(atomicStampedReference.compareAndSet(101, 100,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1) + "\t" + Thread.currentThread().getName() + "\t");
            System.out.println(Thread.currentThread().getName() + "\t当前版本号:" + atomicStampedReference.getStamp());

        }, "T1").start();


        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);
            // t2 线程暂停三秒,确保 t1 线程能够进行假ABA操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "\t 目前使用的版本号是:" + stamp);
            System.out.println(atomicStampedReference.compareAndSet(100, 2000,
                    stamp,
                    stamp + 1) + "\t" + Thread.currentThread().getName() + "\t");
            System.out.println(Thread.currentThread().getName() + "\t当前实际版本号:" + atomicStampedReference.getStamp());
            System.out.println("\t当前实际最新值:" + atomicStampedReference.getReference());
        }, "T2").start();

    }
}

/*
    T1	第一次版本号:1
    T2	第一次版本号:1
    true	T1	
    T1	当前版本号:2
    true	T1	
    T1	当前版本号:3
    T2	 目前使用的版本号是:1
    false	T2	
    T2	当前实际版本号:3
        当前实际最新值:100
*/
  • 我们可以看出,即使期望值一样,但是版本号没有对应上,这样也没办法成功修改主内存中的值
[root@zhaolimin named]# nslookup aaa.zlm.com
Server:			192.168.216.4
Address:		192.168.216.4#53

Name:	aaa.zlm.com
Address: 192.168.216.2

[root@zhaolimin named]# nslookup 192.168.216.2
2.216.168.192.in-addr.arpa	name = ftp.zlm.com
2.216.168.192.in-addr.arpa	name = aaa.zlm.com

[root@zhaolimin named]#

码云仓库同步笔记,可自取欢迎各位star指正:https://gitee.com/noblegasesgoo/notes

如果出错希望评论区大佬互相讨论指正,维护社区健康大家一起出一份力,不能有容忍错误知识。
										—————————————————————— 爱你们的 noblegasesgoo
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值