一、前言
我们在面试的时候,经常性的会遇到一些关于锁的问题,尤其是面试官会提出问题:你对锁的了解多么?你知道锁的原理么?等等问题,于是也就会有后续延伸出来的:你知道CAS么?你知道什么是自旋么?
二、自旋
自旋,顾名思义,可以理解为“自我旋转”,放到程序中就是“自我循环”,比如while循环或者for循环。结合者锁来理解的话就是:先获取一次锁,如果获取不到锁,会不停的循环获取,直到获取到。++不像普通锁那样,获取不到锁就进入阻塞状态++。
三、CAS
CAS是什么?它的英文全程是Compare-And-Swap,中文叫“比较并交换”,它是一种思想、一种算法。
CAS算法有3个基本操作数:
- 内存地址V
- 旧的预期值A
- 要修改的新值B
在并发情况下,各个代码的执行顺序不能确定,为了保证并发安全,哦我们可以使用普通的互斥锁,比如Java的synchronized、ReentrantLock等。而CAS的特点就是:避免使用互斥锁,当多个线程并发使用CAS更新同一个变量时,只有一个可以操作成功,其他都会失败;而且使用CAS更新失败的线程并不会阻塞,会快速失败并返回一个失败的状态,允许你再次尝试。
而CSA(Compare-And-Swap)时一种原子操作,用于实现多线程环境下的同步和并发控制。其基本原理如下:
- 读取内存值:首先,CAS会读取内存中的一个变量的当前值。
- 比较内存值和预期值:接下来,CAS会将读取的值与预期值进行比较,如果两者相等,则说明内存中的值并没有被其他线程修改。
- 如果相等,则将新值写入内存:在比较阶段,如果发现内存值与预期值相等,CAS会尝试将新值写入内存中。这个写入操作是原子的,即在这个过程中,不会被其他线程中断
- 如果写入成功,则操作完成;否则重复上述步骤:如果写入操作成功,CAS完成。如果写入操作失败,说明在比较和写入的过程中,内存值已经被其他线程修改,此时需要重新执行整个CAS操作。
可是使用下图来帮助理解:
在上图中涉及到三个值的比较和操作:修改之前获取的(待修改)值A,业务逻辑计算的新值B,以及待修改值对应的内存位置的C。
整个处理流程中,假设内存中存在一个变量i,它在内存中对应的值是A(第一次读取),此时经过业务处理之后,要把它更新成B,那么在更新之前会再读取一下i现在的值C,如果在业务处理的过程中i的值并没有发生变化,也就是A和C相同,才会把i更新(交换)为新值B。如果A和C不相同,那说明在业务计算时,i的值发生了变化,则不更新(交换)成B。最后,CPU会将旧的数值返回。而上述的一系列操作由CPU指令来保证是原子的。
更简单点来说就是:如果我想更新一个变量A,那么我先把这个变量A记下来,等我处理了一些业务逻辑以后,准备更新这边变量了,我再读取一下这个变量A的值,比较一下,是否和之前读取的值相等 (验证在我处理业务的过程中是否被其他线程更改了),如果值相等,则更新变量A的值。(注意这里的描述:值相等)
CAS的基本原理就是利用比较和写入的原子性操作来实现对共享变量的原子操作,从而避免了传统锁机制中的死锁和线程阻塞问题。
四、自旋锁和CAS的关系是什么呢?
其实他们是两个不同的概念:
- 自旋是一种锁的优化机制:在锁优化中『自旋锁』指线程空转重试获取锁,避免线程上下文切换带来的开销。
- CAS是一种乐观锁机制,cas是通过比较并交换,失败的时候可以直接返回false而不用自旋的获取。只是在一般应用场景下,cas会带有重试机制(while或for实现空转,不断尝试获取)。
若硬有关系,那可以这么理解:自旋锁 = 循环 + CAS
五、CSA的缺点
我们都知道了这个自旋锁和 CAS 的关系了,那么CAS 都有哪些缺点呢?Compare-And-Swap (CAS) 的缺点包括:
- 自旋等待:CAS 在执行时会进行自旋等待,如果失败则需要重试,这会消耗处理器资源。
- ABA 问题:CAS 只能检测到共享变量的值是否发生了变化,但无法检测到变量的值是否经历了类似 A->B->A 的变化,这可能导致一些意外的问题。
- 无法保证公平性:CAS操作是非阻塞的,因此无法保证等待线程的公平性,可能导致某些线程长时间无法获取到执行机会。
- 无法解决死锁:CAS无法解决死锁问题,如果多个线程同事执行CAS操作,可能导致死锁的发生。
- 限制性: CAS操作通常只能应用于单个变量,对于复杂的数据结构,需要额外的处理来实现原子操作。
总的来说,CAS 虽然具有高效的特点,但也存在着一些局限性和缺点。
既然我们说了这个 CAS 那么面试官不可避免的就会问到,既然你了解了 CAS ,那么你是不是也对 ABA 问题有了解呢?
六、ABA问题
还记得我们前文提到的“值相等”描述吗?如果在CAS处理业务期间,一个变量的值经理了以下变化:A–>B–>A 。那么CAS认为这个变量并没有发生变化,因为其读取到的结果是一样的,但是这个变量确实被其他线程修改过了。
这就是CAS中著名的ABA问题,我们先来了了解一下什么叫ABA问题:
6.1 什么是ABA问题
ABA问题是在分布式系统中常见的一种数据一致性问题。它的名称来源于三个操作:A(原始值)、B(第一个读取)、A(第二个读取)。ABA问题发生在一个线程T1读取了一个共享变量的值A,然后另一个线程T2修改了这个共享变量的值为B,然后又改回A,最后线程T1再次读取这个共享变量的值,发现仍然是A。在这种情况下,线程T1可能会错误地认为共享变量的值没有改变,从而导致数据不一致。
6.2 如何解决ABA问题
解决ABA问题的常见方案是:使用版本号或者标记来跟踪数据的变化。通过在每次数据变化时增加版本号或者标记,可以避免ABA问题的发生,即:
在类变量前追加上版本号,每次变量更新的时候把版本号+1,那么A-B-A就会变成1A-2B-3A。
另外,从Java1.5开始JDK的atomic包里提供的AtomicStampedReference类和AtomicMarkableReference类能够解决CAS的ABA问题。