初识CAS锁(概述、底层原理、原子引用、自旋锁、缺点)

目录

一、什么是CAS锁?

1、释义

2、示例代码

3、源码分析compareAndSet(int expect,int update)

二、CAS底层原理

1、Unsafe

2、valueOffset

3、volatile

4、源码分析

5、底层汇编

6、总结

三、原子引用

四、自旋锁,借鉴CAS思想

1、什么是自旋锁?

2、示例

五、CAS的缺点

1、循环时间长开销很大

2、引出来ABA问题

2.1、ABA问题案例   


一、什么是CAS锁?

        CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值。经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。 简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换

1、释义

CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。

当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来 。

CAS是JDK提供的非阻塞原子性操作,它通过 硬件保证了比较-更新的原子性。

它是非阻塞的且自身原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠

2、示例代码

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        //期望时5,如果是5则改成2022
        System.out.println(atomicInteger.compareAndSet(5, 2022) + "\t" + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 2022) + "\t" + atomicInteger.get());
    }
}

 输出结果

由于前面修改了,后面修改失败,故先true后false。

3、源码分析compareAndSet(int expect,int update)

上面三个方法都是类似的,主要对4个参数做一下说明。

var1:表示要操作的对象

var2:表示要操作对象中属性地址的偏移量

var4:表示需要修改数据的期望的值

引出来一个问题:UnSafe类是什么?

二、CAS底层原理

1、Unsafe

        unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。 Unsafe类存在于sun.misc包中 ,其内部方法操作可以像C的指针 一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

2、valueOffset

变量valueOffset,表示该变量值在内存中的 偏移地址 ,因为Unsafe就是根据内存偏移地址获取数据的

3、volatile

变量value用volatile修饰,保证了多线程之间的内存可见性。

4、源码分析

OpenJDK源码里面查看下 Unsafe.java

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上):

  • AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  • 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  • 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B 没有被挂起 并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  • 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败, 只能重新读取重新来一遍了
  • 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

5、底层汇编

native修饰的方法代表是底层方法

Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt"); 
  oop p = JNIHandles::resolve(obj); 
// 先想办法拿到变量value在内存中的地址,根据偏移量valueOffset,计算 value 的地址 
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); 
// 调用 Atomic 中的函数 cmpxchg来进行比较交换,其中参数x是即将更新的值,参数e是原内存的值 
  return (jint) (Atomic::cmpxchg(x, addr, e)) == e; 
UNSAFE_END 

(Atomic::cmpxchg(x, addr, e)) == e; (主要源码)

cmpxchg

调用 Atomic 中的函数 cmpxchg来进行比较交换,其中参数x是即将更新的值,参数e是原内存的值

return (jint) (Atomic::cmpxchg(x, addr, e)) == e;

unsigned Atomic:: cmpxchg (unsigned int exchange_value,volatile unsigned int* dest, unsigned int compare_value) {
    assert(sizeof(unsigned int) == sizeof(jint), "more work to do"); 
   /* 
   * 根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载函数*/ 
    return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest, (jint)compare_value); 
} 

6、总结

  • CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性
  • 实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令

核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来。

三、原子引用

在上面我们知道AtomicInteger原子整型,那可否有其它原子类型呢?

比如说:AtomicBook、AtomicOrder

答案是肯定的。这里引入AtomicReference

AtomicReference示例:

@Data
@AllArgsConstructor
class User{
    String username;
    int age;
}
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User z3 = new User("z3", 24);
        User li4 = new User("li4", 26);

        AtomicReference<User> atomicReferenceUser = new AtomicReference<>();

        atomicReferenceUser.set(z3);
        System.out.println(atomicReferenceUser.compareAndSet(z3, li4) + "\t" + atomicReferenceUser.get().toString());
        System.out.println(atomicReferenceUser.compareAndSet(z3, li4) + "\t" + atomicReferenceUser.get().toString());
    }
}

四、自旋锁,借鉴CAS思想

1、什么是自旋锁?

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式 去尝试获取锁 ,

当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU 。

2、示例

题目:实现一个自旋锁
自旋锁好处:循环比较获取没有类似 wait 的阻塞。
通过 CAS 操作完成自旋锁。

public class SpinLockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t" + "---come in");
        while (!atomicReference.compareAndSet(null, thread)) {

        }
    }

    public void unlock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\t" + "---task over , unlock ...");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.lock();
            try {
                TimeUnit.MILLISECONDS.sleep(5000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            spinLockDemo.unlock();
        }, "A").start();

        new Thread(() -> {
            spinLockDemo.lock();

            spinLockDemo.unlock();
        }, "B").start();

    }
}

输出结果

A线程先进,B线程后进。紧接着A线程等待,然后解锁,B线程在A线程解锁后才会解锁。

解析:

  • A 线程先进来调用 myLock 方法自己持有锁 5 秒钟
  • B 随后进来后发现当前有线程持有锁,不是 null ,所以只能通过自旋等待,直到 A 释放锁后 B 随后抢到。

这种自旋等待尝试的过程就是自旋锁。

五、CAS的缺点

1、循环时间长开销很大

我们可以看到getAndAddInt方法执行时,有个do while 。

如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

2、引出来ABA问题

  • 线程A从主内存中拿了一份值 , 假如是10 , 它在自己的工作空间中打算进行修改了 (还没修改)
  • 这时线程B抢占了线程A , 它从主内存中将10取出来了, 它给修改成了22 , 放回主内存中了 , 但是呢 ,它感觉修改的不满意 , 又去主内存把那个22又取出来了 , 又给改成了10 , 然后走了
  • 这时线程A 去进行比较并交换 , 发现工作内存值和主内存值(期望值)是一样的都是10 , 就成功修改了, 但是 ! 注意 ! 线程A并不知道这个值中途被其他线程修改了 ! ! !        

2.1、ABA问题案例   

        假设有一个遵循CAS的取款机,小明去取款机取款,此使账户余额100元,小明要取款50,结果取钱的时候机器卡了以下,你多按了几下取钱操作,ATM就创建了多个线程来进行扣款操作,并且该扣款操作是基于CAS来实现的   

  • 线程1:取款50,余额50
  • 线程2:取款50,余额50(失败)
    理想状况下之只能有一个线程执行成功,另一个线程执行失败,进入等待,但是此时小明的母亲给小明转账50元,此时:
  • 线程3:存款50,余额100元
    由于线程3执行成功,小明账户余额又到达了100元,此时线程2,通过cas比较交换,发现前后数据一致,所以可以进行数据改写.如下:
  • 线程2:取款50,余额50
    此时不难发现小明的余额少了50元.这就是ABA问题.那么我们如何解决呢?
  • 我们可以使用CAS中的"原子引用"来解决ABA问题.

        原子引用是基于乐观锁的思想,为CAS中的每个操作添加一个版本号,每次执行成功之后,都会对版本号进行+1操作,每次更新数据之前都会对比版本号,观察是否有其他线程进行干扰.如果版本号一制则继续,不一致进入自旋.

        Java中提供了AtomicStampedReference类来实现原子引用并可以设置版本号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值