面试必问系列:悲观锁和乐观锁的那些事儿

元宇宙开发者
• 阅读 3563

面试必问系列:悲观锁和乐观锁的那些事儿

程序安全

线程安全是程序开发中非常需要我们注意的一环,当程序存在并发的可能时,如果我们不做特殊的处理,很容易就出现数据不一致的情况。

通常情况下,我们可以用加锁的方式来保证线程安全,通过对共享资源 (也就是要读取的数据) 的加上"隔离的锁",使得多个线程执行的时候也不会互相影响,而悲观锁乐观锁正是并发控制中较为常用的技术手段。

乐观锁和悲观锁

什么是悲观锁?什么是乐观锁?其实从字面上就可以区分出两者的区别,通俗点说,

悲观锁

悲观锁就好像一个有迫害妄想症的患者,总是假设最坏的情况,每次拿数据的时候都以为别人会修改,所以每次拿数据的时候都会上锁,直到整个数据处理过程结束,其他的线程如果要拿数据就必须等当前的锁被释放后才能操作。

使用案例

悲观锁的使用场景并不少见,数据库很多地方就用到了这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁,悲观锁的实现往往依靠数据库本身的锁功能实现。Java程序中的Synchronized和ReentrantLock等实现的锁也均为悲观锁。

在数据库中,悲观锁的调用一般是在所要查询的语句后面加上 for update

select * from db_stock where goods_id = 1 for update

当有一个事务调用这条 sql 语句时,会对goods_id = 1 这条记录加锁,其他的事务如果也对这条记录做 for update 的查询的话,那就必须等到该事务执行完后才能查出结果,这种加锁方式能对读和写做出排他的作用,保证了数据只能被当前事务修改。

当然,如果其他事务只是简单的查询而没有用 for update的话,那么查询还是不会受影响的,只是说更新时一样要等待当前事务结束才行。

值得注意的是,MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交,就是说,如果我们不仅要读,还要更新数据的话,需要手动控制事务的提交,比如像下面这样:

set autocommit=0;
//开始事务
begin;
//查询出商品id为1的库存表数据
select * from db_stock where goods_id = 1 for update;
//减库存
update db_stock set stock_num = stock_num - 1 where goods_id = 1 ;
//提交事务
commit;

虽然悲观锁能有效保证数据执行的顺序性和一致性,但在高并发场景下并不适用,试想,如果一个事务用悲观锁对数据加锁之后,其他事务将不能对加锁的数据进行除了查询以外的所有操作,如果该事务执行时间很长,那么其他事务将一直等待,这无疑会降低系统的吞吐量。

这种情况下,我们可以有更好的选择,那就是乐观锁。

乐观锁

乐观锁的思想和悲观锁相反,总是假设最好的情况,认为别人都是友好的,所以每次获取数据的时候不会上锁,但更新数据那一刻会判断数据是否被更新过了,如果数据的值跟自己预期一样的话,那么就可以正常更新数据。

场景

这种思想应用到实际场景的话,可以用版本号机制和CAS算法实现。

CAS

CAS是一种无锁的思想,它假设线程对资源的访问是没有冲突的,同时所有的线程执行都不需要等待,可以持续执行。如果遇到冲突的话,就使用一种叫做CAS (比较交换) 的技术来鉴别线程冲突,如果检测到冲突发生,就重试当前操作到没有冲突为止。

原理

CAS的全称是Compare-and-Swap,也就是比较并交换,它包含了三个参数:V,A,B,V表示要读写的内存位置,A表示旧的预期值,B表示新值

具体的机制是,当执行CAS指令的时候,只有当V的值等于预期值A时,才会把V的值改为B,如果V和A不同,有可能是其他的线程修改了,这个时候,执行CAS的线程就会不断的循环重试,直到能成功更新为止。

面试必问系列:悲观锁和乐观锁的那些事儿

正是基于这样的原理,CAS即时没有使用锁,也能发现其他线程对当前线程的干扰,从而进行及时的处理。

缺点

CAS算是比较高效的并发控制手段,不会阻塞其他线程。但是,这样的更新方式是存在问题的,看流程就知道了,如果C的结果一直跟预期的结果不一样的话,线程A就会一直不断的循环重试,重试次数太多的话对CPU也是一笔不小的开销。

而且,CAS的操作范围也比较局限,只能保证一个共享变量的原子操作,如果需要一段代码块的原子性的话,就只能通过Synchronized等工具来实现了。

除此之外,CAS机制最大的缺陷就是"ABA"问题。

ABA问题

前面说过,CAS判断变量操作成功的条件是V的值和A是一致的,这个逻辑有个小小的缺陷,就是如果V的值一开始为A,在准备修改为新值前的期间曾经被改成了B,后来又被改回为A,经过两次的线程修改对象的值还是旧值,那么CAS操作就会误任务该变量从来没被修改过,这就是CAS中的“ABA”问题。

面试必问系列:悲观锁和乐观锁的那些事儿

看完流程图相信也不用我说太多了吧,线程多发的情况下,这样的问题是非常有可能发生的,那么如何避免ABA问题呢?

加标志位,例如搞个自增的字段,没操作一次就加一,或者是一个时间戳,每次更新比较时间戳的值,这也是数据库版本号更新的思想(下面会说到)

在Java中,自JDK1.5以后就提供了这么一个并发工具类AtomicStampedReference,该工具内部维护了一个内部类,在原有基础上维护了一个对象,及一个int类型的值(可以理解为版本号),在每次进行对比修改时,都会先判断要修改的值,和内存中的值是否相同,以及版本号是否相同,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

适用场景

CAS一般适用于读多写少的场景,因为这种情况线程的冲突不会太多,也只有线程冲突不严重的情况下,CAS的线程循环次数才能有效的降低,性能也能更高。

版本号机制

版本号机制是数据库更新操作里非常实用的技巧,其实原理很简单,就是获取数据的时候会拿一个能对应版本的字段,然后更新的时候判断这个字段是否跟之前拿的值是否一致,一致的话证明数据没有被别人更新过,这时就可以正常实现更新操作。

还是上面的那张表为例,我们加上一个版本号字段version,然后每次更新数据的时候就把版本号加1,

select goods_id,stock_num,version from db_stock where goods_id = 1

update db_stock set stock_num = stock_num - 1,version = version + 1 where goods_id = 1 and version = #{version}

这样的话,如果有两个事务同时对goods_id = 1这条数据做更新操作的话,一定会有一个事务先执行完成,然后version字段就加1,另一个事务更新的时候发现version已经不是之前获取到的那个值了,就会重新执行查询操作,从而保证了数据的一致性。

这种锁的方式也不会影响吞吐量,毕竟大家都可以同时读和写,但高并发场景下,sql更新报错的可能性会大大增加,这样对业务处理似乎也不友好。

这种情况下,我们可以把锁的粒度缩小,比如说减库存的时候,我们可以这么处理:

update db_stock set stock_num = stock_num - 1  where goods_id = 1 and stock_num > 0

这样一来,sql更新冲突的概率会大大降低,而且也不用去单独维护类似version的字段了。

最后

关于悲观锁和乐观锁的例子介绍就到这儿了,当然,本文也只是略微讲解,更多的知识点还要靠大家研究,而且,除了这两种锁,并发控制中还有很多其他的控制手段,像什么Synchronized、ReentrantLock、公平锁,非公平锁之类的都是很常见的并发知识,不管是为了日常开发还是应付面试,掌握这些知识点还是很有必要的,而且,并发编程的知识思想是共通的,知道一块知识点后很容易就能延伸去学习其他的知识点。

拿我自己来说,最近也在认真研究Java并发编程的一些知识点,也因为要写乐观锁的缘故,顺道复习了一下CAS和它的使用案例,从而也了解到了ReentrantLock底层其实就是通过CAS机制来实现锁的,而且还了解了独占锁,共享锁,可重入锁等使用场景,由点到面,也让我知识体系储备更加的丰富,近期也有打算撸几篇关于ReentrantLock知识的文章出来,欢迎大家多来踩踩!


作者:鄙人薛某,一个不拘于技术的互联网人,技术三流,吹水一流,想看更多精彩文章可以关注我的公众号哦~~~

面试必问系列:悲观锁和乐观锁的那些事儿


原创不易,您的 【三连】 将是我创作的最大动力,感谢各位的支持!

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
梦
4年前
微信小程序new Date()转换时间异常问题
微信小程序苹果手机页面上显示时间异常,安卓机正常问题image(https://imghelloworld.osscnbeijing.aliyuncs.com/imgs/b691e1230e2f15efbd81fe11ef734d4f.png)错误代码vardate'2021030617:00:00'vardateT
Stella981 Stella981
3年前
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解2016年09月02日00:00:36 \牧野(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fme.csdn.net%2Fdcrmg) 阅读数:59593
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
美凌格栋栋酱 美凌格栋栋酱
5个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
元宇宙开发者
元宇宙开发者
Lv1
近乡情更怯,不敢问来人。
文章
4
粉丝
0
获赞
0