JUC并发编程之CAS

177 阅读5分钟

前言

最近博主接触了下JUC并发编程,因此想把最近学习的知识整理一下分享给大家,以便于大家对JUC有一个了解,本文主要就是谈一谈JUC里面CAS。

CAS简介

所谓的CAS就是CompareAndSwap缩写,中文翻译为比较并交换,是我们并发算法时比较常用的一个技术。
它包含了三个操作数——内存位置、预期原值和更新值。

执行CAS操作的时候,会将内存位置的值和预期原值比较,如果两个值一样就更改为更新值,如果不一样就不做任何操作。因此我们很容易知道,只有当内存位置的值和我们预期的值一样才会交换,否则就会交换失败。

这个有点像我们买东西,很多时候我们有自己的预算,本来按照原本的价格我们的预算是够的,但是有一天,哎,突然涨价了,跟我们的预算不一样,那不好意思我就不买了。但是我们有时候就是非常想要,因此我们会一直等,每次都来看一看你变回原来的价格没?这个行为就叫做自旋

image.png

实际上这张图也不尽准确,自旋的时候会再次从内存位置获取值(因为其他线程完全可能将内存位置的值修改,修改后的值可能就跟我们的预期值一样了),再重新判断。

硬件级别的保证

首先明确的一点就是:CAS是非阻塞且自身具有原子性,因此他效率更高且通过硬件保证,更可靠。然后它还是一条CPU的原子指令(cmpxchg指令),不会造成数据不一致问题,我们JUC中一个很重要的类——Unsafe类,它提供的CAS方法(如compareAndSwapInt)底层实现就是cpu指令cmpxchg。执行这个指令的时候,会判断当前的系统是不是多核系统,如果是就给总线加锁,只有一个线程可以对总线加锁成功,加锁成功后会执行CAS操作,换句话说CAS的原子性实际上是CPU独占的,比用Synchronized重量级锁的排他时间更短,这也就是为什么他在多线程情况下性能会比较好。

image.png

原子引用

我们都知道有原子整型,那么有没有原子引用类型呢?当然有那就是AtomicReference类型。这里我们通过一个原子引用类型来看看CAS的具体应用。

来,上代码!

package com.ashao.JUC.CAS;

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

public class CASDemo {
    public static void main(String[] args) {
        AtomicReference<User> atomicReference = new AtomicReference<>();
        User expectUser = new User();
        User zhang3 = new User("zhang3", 22);
        User lisi = new User("lisi",24);

        new Thread(()->{
            atomicReference.set(expectUser);
            System.out.println(Thread.currentThread().getName() + "\t 线程将预期User设为:" + atomicReference.get());
            while(true){
                //没过2秒查询一次
                try{TimeUnit.MILLISECONDS.sleep(500);}catch(InterruptedException e){e.printStackTrace();}
                System.out.println("atomicReference内的user对象为:" + atomicReference.get());
            }

        },"t1").start();

        new Thread(()->{
            //睡眠一秒确保expectUser被设置成功
            try{
                TimeUnit.SECONDS.sleep(1);}catch(InterruptedException e){e.printStackTrace();}

            boolean b = atomicReference.compareAndSet(zhang3, lisi);
            if (!b){
                System.out.println(Thread.currentThread().getName() + "\t 线程修改失败,预期值不符合");
            }else {
                System.out.println(Thread.currentThread().getName() + "\t 线程将预期User设为:" + atomicReference.get());
            }

        },"t2").start();

        new Thread(()->{
            //睡眠2秒确保t2中的compareAndSet方法先执行
            try{
                TimeUnit.SECONDS.sleep(2);}catch(InterruptedException e){e.printStackTrace();}
            boolean b = atomicReference.compareAndSet(expectUser, zhang3);
            if (!b){
                System.out.println(Thread.currentThread().getName() + "\t 线程修改失败,预期值不符合");
            }else {
                System.out.println(Thread.currentThread().getName() + "\t 线程将预期User设为:" + atomicReference.get());
            }

        },"t3").start();

    }
}
class User{
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public User() {
    }

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

image.png

我们可以看到t2线程因为不符合预期值,因此lisi对象没有被更新进去,而t3线程符合预期值,expectUser被更新为zhang3.

自旋锁

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,自旋在这里是指尝试获得锁的线程不会立即阻塞,而是采用循环的方法去尝试获取锁,当线程发现锁被占用的时候,就会不断循环判断锁的状态,直到获取。这样做的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

咱们模拟一下自旋锁。来,上代码!

package com.ashao.JUC.CAS;

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

public class CASLockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
    public static void main(String[] args) {
        CASLockDemo casLock = new CASLockDemo();

        new Thread(()->{
            casLock.lock();
            //暂停几秒钟线程
            try{
                TimeUnit.SECONDS.sleep(4);}catch(InterruptedException e){e.printStackTrace();}
            casLock.unlock();

        },"A").start();

        //暂停500毫秒线程,让A线程先于B线程启动
        try{
            TimeUnit.MILLISECONDS.sleep(500);}catch(InterruptedException e){e.printStackTrace();}

        new Thread(()->{
            casLock.lock();
            casLock.unlock();
        },"B").start();
    }

    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t ------come in");
        while(!atomicReference.compareAndSet(null,thread)){
            System.out.println("正在尝试重新获取锁...");
        }
    }
    
    public void unlock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName() + "\t ---task over,unlock");
    }
}

image.png

当A线程率先获取到锁后,B线程再去获取锁就会获取失败并且一直自旋获取,直到A线程释放了锁,B线程才能获取成功。

CAS两大缺点

  • 循环时间长开销很大
    就拿AtomicInteger类的getAndInt方法举例:

image.png 我们可以很清楚的看到这个方法内部有个do-while,如果CAS失败,他就会一直尝试,时间长了就会给cpu带来很大开销

  • ABA问题

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差就可能会产生数据的变化。

比如说线程1从内存位置取出v值,这时候线程2也从内存中取出v值,并且线程2进行了一些操作将值变成了B,然后线程2又将v数据变成A,这时候线程1进行了CAS操作发现内存中的值仍然是A,线程A操作成功,但是,注意!但是虽然执行成功了,这并不代表没问题。

这就好像,你出差去了外地,出差前你老婆在家,但是出差期间,万恶的老王来找你老婆了,然后...在你回来之前他拍拍屁股走人了,完事你没发现啥异样,但是这是能被允许的吗?同理在CAS中也是一样。

那么解决方案是什么?有,大大滴有!那就是——版本号戳记,你只要修改了就得设置版本。

image.png

结语

至此我们已经初步的认识了CAS,希望本文能帮助到大家!!
各位下次再见!

微信图片_20220901111409.jpg