【高频面试】锁与CAS详解(⭐建议收藏) 您所在的位置:网站首页 自旋锁和cas的区别是什么 【高频面试】锁与CAS详解(⭐建议收藏)

【高频面试】锁与CAS详解(⭐建议收藏)

2024-07-06 02:23| 来源: 网络整理| 查看: 265

🔥【高频面试】锁🔒与CAS详解 ❤️‍ 大家好,我是java厂长,今天带你们了解高频面试之Java锁🔒!❤️‍ 关于作者 作者介绍

🍓 博客主页:作者主页 🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆。 🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻。

文章目录 🔥【高频面试】锁🔒与CAS详解关于作者一. 悲观锁与乐观锁二、实现方式1)CAS(Compare And Swap)2)版本号机制 三、面试官问:乐观锁加锁吗?四、面试官问:CAS有哪些缺点?1)一次性只能保证一个共享变量的原子性2)循环会耗时3)存在ABA问题(重点) 五、适用场景1)功能限制2)竞争激烈程度 此篇博文对Java学习理解底层很有帮助!

一. 悲观锁与乐观锁

​ 乐观锁和悲观锁问题,是出现频率比较高的面试题。本文将由浅入深,逐步介绍它们的基本概念、实现方式(含实例)、适用场景,以及可能遇到的面试官追问,希望能够帮助你打动面试官。

​ 乐观锁和悲观锁是两种思想,主要是解决并发场景下的数据争夺的问题。

乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。 二、实现方式

​ 悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。

​ 乐观锁的实现方式有两种:CAS机制和版本号机制。

1)CAS(Compare And Swap)

​ CAS的原理很简单,包含三个值

需要读写的内存位置(V)预期原来的值(A)期待更新的值(B)。

如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,返回true。否则处理器不做任何操作,返回false。

实现CAS最重要的一点,就是比较和交换操作的一致性,否则就会产生歧义。

CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。

这里引出一个新的问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

比如当前线程比较成功后,准备更新共享变量值的时候,这个共享变量值被其他线程更改了,那么CAS函数必须返回false。

要实现这个需求,java中提供了Unsafe类,它提供了三个函数,分别用来操作基本类型int和long,以及引用类型Object。

在这里插入图片描述

参数的意义:

var1和 var2:表示这个共享变量的内存地址。这个共享变量是var1对象的一个成员属性,var2表示这个共享变量在var1类中的内存偏移量。所以通过这两个参数就可以直接在内存中修改和读取共享变量值。

var4: 表示预期原来的值。

var5: 表示期待更新的值。

并发比较低的时候用CAS比较合适,并发比较高用synchronized比较合适。

接下来以Java中的自增操作( i++ )为例,看一下悲观锁和CAS分别是如何保证线程安全的。在Java中自增操作不是原子操作,它实际上包含三个独立的操作:第一步是读取i值;第二步是加1;第三步是将新值赋值给i

package com.zmz.lock; import java.util.concurrent.atomic.AtomicInteger; /** * @ProjectName: Juc * @Package: com.zmz.lock * @ClassName: LockTest * @Author: 张晟睿 * @Date: 2021/10/17 14:50 * @Version: 1.0 */ public class LockTest { //线程不安全 private static int num1 = 0; //使用乐观锁 private static AtomicInteger num2 = new AtomicInteger(0); //使用悲观锁 private static int num3 = 0; private static synchronized void addNum3(){ num3++; } public static void main(String[] args) throws Exception { //开启2000个线程 自增 for(int i = 0; i 数据库记录当前版本,数据被更新,并且数据库记录 version 更新为 2 。操作员 B 完成了操作,也将版本号+1( version=2 )试图向数据库提交数据( balance=$70 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 当前最后更新的version与操作员第一次的版本号相等 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。 三、面试官问:乐观锁加锁吗?

在面试时,曾遇到面试官如此追问。下面是我对这个问题的理解:

(1)乐观锁本身是不加锁的,只是在更新数据的时候会判断一下数据是否被其他线程已经更新过了

(2)有时乐观锁可能与加锁操作两者同时使用

四、面试官问:CAS有哪些缺点?

面试到这里,我可能就要恭喜你大概率是面试通过了🥰🥰🥰🥰,面试官可能已经中意你了。不过面试官准备对你发起最后的进攻:你知道CAS这种实现方式有什么缺点吗?

1)一次性只能保证一个共享变量的原子性

​ 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

2)循环会耗时

​ 我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

​ 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。当然,更重要的是避免在高竞争环境下使用乐观锁。

3)存在ABA问题(重点)

​ 先简单解释一下什么是ABA

​ 假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:

​ (1)线程1读取内存中数据为A;

​ (2)线程2将该数据修改为B;

​ (3)线程2将该数据修改为A;

​ (4)线程1对数据进行CAS操作

​ 在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。

​ 在AtomicInteger的例子中,ABA似乎没有什么危害。但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。

​ 对于ABA问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。所以JAVA中提供了AtomicStampedReference/AtomicMarkableReference来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。

​ 问题:如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?

​ 如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”(原子标记参考 ),它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚"ABA"问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

五、适用场景

​ 乐观锁和悲观锁并没有优劣之分,它们有各自适合的场景;下面从两个方面进行说明。

1)功能限制

​ 与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。

​ 例如,CAS只能保证一个共享变量的原子操作,当涉及到多个变量时,CAS是无能为力的,而 synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

2)竞争激烈程度

​ 如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

​ 1️⃣当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。

​ 2️⃣当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

希望对各位的面试有帮助,也希望小伙伴们多多支持厂长,留下你们的爱心💕和赞👍! 💗最后厂长祝大家能够拿到心仪的Offer💗


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有