锁与CAS详解

2023-05-16

一、悲观锁与乐观锁

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

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

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

二、实现方式

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

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

2.1、CAS(Compare And Swap)

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

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

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

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

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

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

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

(6)要实现这个需求,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;

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 < 2000; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    num1++;
                    num2.getAndIncrement();
                    addNum3();
                }
            }).start();
        }

        Thread.sleep(2000);//休眠2s
        System.out.println("1、线程不安全:" + num1);
        System.out.println("2、乐观锁(AtomicInteger):" + num2);
        System.out.println("3、悲观锁(synchronized):" + num3);
    }
}

运行结果 

在这里插入图片描述

通过实验,我们发现并发执行自增操作,导致计算结果的不准确。在上面的代码测试中:num1没有进行任何线程安全方面的保护,num2使用了乐观锁(CAS),num3使用了悲观锁(synchronized)。运行程序,使用2000个线程同时对num1、num2和num3进行自增操作,可以发现:num2和num3的值总是等于2000,而num1的值常常小于2000。

首先来介绍AtomicInteger。AtomicInteger是java.util.concurrent.atomic包提供的原子类,利用CPU提供的CAS操作来保证原子性;这个包里面提供了一组原子变量类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。除了AtomicInteger外,还有AtomicBoolean、AtomicLong、AtomicReference等众多原子类。

下面看一下AtomicInteger的源码,了解下它的自增操作getAndIncrement()是如何实现的(JAVA8)

在这里插入图片描述

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    //Unsafe用于实现对底层资源的访问
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //valueOffset是value在内存中的偏移量
    private static final long valueOffset;
    
    /**
     * 通过Unsafe获得valueOffset
     * Unsafe类是用来在任意内存地址位置处读写数据,可见,对于普通用户来说,使用起来还是比较危险的。
     * public native long objectFieldOffset(Field var1);方法用于获取某个字段相对Java对象的“起始地址”的偏移量,方法返回值和参数如下
     * AtomicInteger.class.getDeclaredField("value")是拿到atomicInteger的value字段的field对象
     * valueoffset是拿到value的相对于AtomicInteger对象的地址偏移量
     * 
     */
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    
    /**
     * 以原子方式将值设置为给定的更新值
     *
     * @param expect 预期值
     * @param update 新值
     * @return 成功返回true 返回false表明实际值不等于预期值,设置失败
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    
    /**
     * 原子上增加一个当前值。
     *
     * @return 之前的值
     */ 
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
     /*
     getAndAddInt函数的方法体,传进来的var4是1,每调用一次增加1.compareAndSwapInt前面解释过了
     public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
     }

     */
}

2.2、版本号机制

版本号机制也可以用来实现乐观锁。版本号机制的主要思想是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改时,同时读取版本号version的值,若刚才读取到的version值为当前数据库中的version值相等时才更新,则版本号加1;否则重试更新操作,直到更新成功。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。

举一个简单的银行取钱的例子:

假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

  1. 操作员 A 此时读出版本号( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. 接下来在操作员 A 操作的过程中,操作员B 也读入此余额及版本号( version=1 ),并从其帐户余额中扣除 $30 ( $100-$30 )。
  3. 操作员 A 完成了修改工作,将数据版本号加一,此时版本号( version=2 )、帐户余额( balance=$50 ),提交至数据库更新,此时,提交数据版本 > 数据库记录当前版本,数据被更新,并且数据库记录 version 更新为 2 。
  4. 操作员 B 完成了操作,也将版本号+1( version=2 )试图向数据库提交数据( balance=$70 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 当前最后更新的version与操作员第一次的版本号相等 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
  5. 这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

三、面试官提问

提问1: 乐观锁加锁吗?

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

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

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

提问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问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。

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

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

四、适用场景

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

4.1、功能限制

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

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

4.2、竞争激烈程度

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

  1. ​当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
  2. ​当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

原文地址; 【高频面试】锁与CAS详解(⭐建议收藏)_java厂长的博客-CSDN博客 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

锁与CAS详解 的相关文章

  • shiro-cas------本地配置cas为HTTPS登录

    上一篇 xff1a shiro cas 搭建基础cas服务器 解决上图所提示的问题 xff08 在本地 xff09 xff0c 需要配置https请求 首先给这个服务起个域名 xff1a shiro sso com 配置到本地的host文件
  • CAS服务器搭建

    一 CAS是Central Authentication Service的缩写 xff0c 中央认证服务 xff0c 一种独立开放指令协议 CAS 是 Yale 大学发起的一个开源项目 xff0c 旨在为 Web 应用系统提供一种可靠的单点
  • 数据库锁机制和CAS概念

    这个作者介绍比较清晰 https www cnblogs com X knight p 10669934 html 64 勋爵
  • CAS方式实现单点登录

    单点登录 xff0c 英文是 Single Sign On xff0c 缩写为 SSO 多个站点 192 168 1 20X 共用一台认证授权服务器 192 168 1 110 xff0c 用户数据库和认证授权模块共用 用户经由其中任何一个
  • CAS 服务端的搭建

    上文讲了CAS客户端 xff0c 本文记录CAS Server的搭建步骤 CAS Server的版本一定要选好 xff0c 我选的是CAS5 3 xff0c Java版本用的8 xff0c 目前最新的CAS6 5的Java版本最低是11了
  • cas 配置相关

    默认配置 span class token comment span span class token comment CAS Cloud Bus Configuration span span class token comment sp
  • SSO、CAS、OAuth、OIDC

    参考 简单了解概念 xff1a https www bilibili com video BV1XG411w7DN 简单了解操作 xff1a https www bilibili com video BV1334y11739 openid
  • cas-overlay-template-5.3 集成Oauth2.0

    第一步 在pom xml 文件添加oauth2 0协议支持
  • Cas5.3服务器集成DM8 达梦数据库

    DM8达梦数据库相关准备 1 安装DM8达梦数据库并安装相关数据库实例 省略一千字 2 新建ucas auth user表 并增加相关用户条记录 DROP TABLE IF EXISTS ucas auth user CREATE TABL
  • spring mvc配置cas客户端

    1 在WEB INF lib中导入cas jar包 点击打开链接 2 导入证书 1 将证书server cer文件拷贝至业务系统所在服务器上的某路径下 如c盘根目录 在cmd命令界面切换至证书所在路径 执行以下命令 jdk路径自行修改 并信
  • Java内存模型,volatile关键字和CAS算法,

    引言 在前几篇博文中我详细介绍了HashMap的底层实现原理 后来我接连写了三天JVM和GC的一些知识 那些知识偏向于理论 今天换点口味 和大家一起研究学习一下ConcurrentHashMap的底层实现 因为jdk1 8在HashMap和
  • CAS5.3 服务器集成MySQL8数据库

    MySQL 相关准备 1 安装MySQL8 并安装相关数据库实例 省略一千字 2 新建ucas auth user表 并增加相关用户条记录 DROP TABLE IF EXISTS ucas auth user CREATE TABLE u
  • loadFromRemoteSourcesenabled="true" // XAML 设计器 // VS 11 beta 和 2012 RC

    我经常被这种情况刺痛 当然总是在最糟糕的时刻 当我编辑 xaml 文件时 收到此错误 System NotSupportedException An attempt was made to load an assembly from a n
  • CAS 服务票证验证失败

    我已点击链接http lukesampson com post 315838839 cas on windows localhost setup in 5 mins 则cas服务器工作正常 登录url为http 10 1 1 26 8080
  • 由 CAS 引起的 APEX 应用程序生成器的编码问题

    我有一段时间对 APEX 应用程序生成器 sql 研讨会有疑问 存在编码问题 APEX 必须提供东欧字符 最后我找到了问题的根源 我已经通过 web xml 将 CAS sso 实现到 APEX 中 APEX 位于 Tomcat ORDS
  • 有没有办法有条件地应用注释?

    在我的 java play 应用程序中 我有注释 RequiresAuthentication clientName CasClient 在我的控制器内 我只想在生产环境中对用户进行身份验证 如何有条件地应用注释 如果我处理身份验证的方式是
  • 如何将CAS认证与Spring Security集成?

    我已将 spring security 集成到我的项目中 并且之前使用 hibernate 验证用户详细信息 现在我必须使用 CAS 来完成它 这是我当前的 Spring security xml
  • 如何在 CAS 服务管理中注册我的 Web 应用程序?

    我的学校想要实现一个CAS服务 我做了大部分配置 但是我需要注册webapp 如何在CAS服务管理中注册webapp 或者如果您有一些可以帮助我的样本 我将非常感激 我尝试过使用 cas 管理应用程序 版本为 4 2 x 我不确定您想使用什
  • Spring Security with CAS 跳过会话固定保护

    我有一个使用 spring security 和 CAS spring 3 0 5 cas 3 4 5 的应用程序 但是当我登录时 会话 ID 没有改变 当我登录时CasAuthenticationFilter执行身份验证 如果身份验证成功
  • 使用 django-cas-ng 在管理站点上进行身份验证

    我在用着Django Cas NG https github com mingchen django cas ng用于验证用户身份的框架 主要问题是管理页面仍然使用默认的登录视图 到目前为止使用的方法 1 使用环境变量 来自文档 CAS A

随机推荐

  • BGP——图解5种报文

    目录 BGP的报文头部 BGP的5种报文 Open 只有邻居建立时会发送此报文 Keepalive 邻居建立时 建立后都会发送此报文 Update 邻居建立成功后才会发送此报文 Notification 邻居建立成功后才会发送此报文 Rou
  • Eth-Trunk链路聚合理论

    目录 什么是Eth trunk Eth Trunk的一些概念 LACP模式 手工模式 链路聚合工作机制 负载分担方式 Eth trunk转发原理 什么是Eth trunk Eth trunk xff08 链路聚合技术 xff09 是一种捆绑
  • Linux 软件包安装

    目录 通过源代码编译安装 通过RPM软件包安装 通过Yum软件仓库安装 配置本地Yum源 通过yum安装软件 通过Dnf软件仓库安装 Linux软件包安装有四种方式 xff1a 源代码 RPM Yum Dnf安装四种方式 通过源代码编译安装
  • MDK + Jlink + SDRAM 裸奔2440教程

    MDK 43 Jlink 43 SDRAM 裸奔2440教程 Author xff1a Aston ofgec deu org 1 背景 很多人学习嵌入式编程都是从裸机开始的 xff08 我个人也是 xff09 这并不是偶然 xff0c 想
  • 谷歌浏览器打开显示2345或360等其他主页的解决方法

    方法如下 xff1a 右键 属性 2 删除圈出来的 Application 应用 重新打开就可以了 如果不行的话 xff0c 进行下面的操作 xff1a 右键快捷方式查看属性 打开文件所在位置 软件重新命名 重复开头的1和2操作就可以了
  • 腾讯运维工程师认证(TCA)认证考试的知识点

    前言 腾讯运维工程师认证 xff08 TCA xff09 认证考试的知识点 xff0c 以下分享以下我的学习笔记 下面是第一章 腾讯运维的概述 一 腾讯云产品概述 1 云网络 包括 xff1a 负载均衡 私有网路 弹性网卡 专线接入 NAT
  • undefined reference to `pthread_create`的解决方法

    在linux中出现一下的报错 test pc c text 43 0x2b7 undefined reference to 96 pthread create 39 test pc c text 43 0x2d2 undefined ref
  • python基础学习值输入A和B,输出

    问题描述 xff1a xff11 输入A B xff0c 输出A 43 B xff12 输入的第一行包括两个整数 xff0c 由空格分隔 xff0c 分别表示A B 代码 xff1a a 61 input split print int a
  • python基础学习数列排序(给定一个长度为n的数列,将这个数列按从小到大的顺序排列。1<=n<=200)

    问题描述 xff1a 给定一个长度为n的数列 xff0c 将这个数列按从小到大的顺序排列 1 lt 61 n lt 61 200 第一行为一个整数n 第二行包含n个整数 xff0c 为待排序的数 xff0c 每个整数的绝对值小于10000
  • python基础学习——十六进制转八进制——蓝桥杯

    目录 1 题目描述 2 思路 3 方法 3 1方法一 3 2方法二 1 题目描述 给定n个十六进制正整数 xff0c 输出它们对应的八进制数 输入的第一行为一个正整数n xff08 1 lt 61 n lt 61 10 xff09 接下来n
  • 手把手教你编写跑马灯——STM32

    一 创建项目 新建一个文件夹 xff0c 打开KeiL projiece new vision projection 给文件命名 xff08 随便取 xff09 根据自己开发板的信号选择对应的型号 在刚才创建的文件夹里面新建一个main c
  • 超详细的python调用树莓派的摄像头基本使用

    这里首先讲解的是CSI摄像头 picamera 1 1录制一段10秒的视频到本地 import picamera camera 61 picamera PiCamera camera resolution 61 640 480 camera
  • 手把手教你写MC9S12G128 Timer

    目录 一 TIM模块概述 1 定时器 计数器的特点 2 嵌入式系统中定时器 计数器模块的作用 二 TIM模块结构和工作原理 1 TIM结构模块 1 1 TIM组成 1 2 特点 2 TIM模块工作原理 2 1 工作模式 2 2 定时器模块框
  • opencv 显示图片

    import cv2 import numpy as np 读取图片 img 61 cv2 imread 39 test16 jpg 39 图片显示 cv2 imshow 39 picture 39 img cv2 waitKey 0 cv
  • VMware+Ubuntu与Windows宿主机文件共享的实现

    学习嵌入式开发 xff0c Linux环境是必不可少的 现在一般都会选择在Windows上用VMware 43 Ubuntu的方式来构筑交叉开发环境 这样的好处是 xff0c 既不离开Windows这个熟悉的环境 xff08 有很多Wind
  • 大数据概论

    一 xff0e 大数据 大数据 xff08 big data xff09 xff0c 指无法在一定时间范围内用常规软件工具进行捕捉 管理和处理的数据集合 xff0c 是需要新处理模式才能具有更强的决策力 洞察发现力和流程优化能力的海量 高增
  • 基于ESP32搭建物联网服务器十三(自已搭建一个MQTT服务器)

    在之前的文章中 ESP32搭建WEB服务器十二 使用MQTT协议与ESP32互动 你的幻境的博客 CSDN博客 我们已经实现了ESP32通过MQTT协议连接到公共MQTT服务器上 xff0c 但是公共服务器在稳定性或安全性上 xff0c 很
  • flash烧写uboot举例

    烧写uboot到QSPIFLASH 简述 xff1a 主要是讲ZYNQ7000系列的烧写文件方法 xff0c 衔接上文链接 Petalinux工具编译生成boot和kernel等流程 JTAG下载全部镜像到内存运行 以下操作在XSDK环境下
  • 1000+ 道 Java面试题及答案整理(2023最新版)

    作为 Java 程序员 xff0c 选择学习什么样的技术 xff1f 什么技术该不该学 xff1f 去招聘网站上搜一搜 看看岗位要求就十分清楚了 xff0c 自己具备的技术和能力 xff0c 直接影响到你工作选择范围和能不能面试成功 如果想
  • 锁与CAS详解

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