面试必问系列:悲观锁和乐观锁的那些事儿
程序安全
线程安全是程序开发中非常需要我们注意的一环,当程序存在并发的可能时,如果我们不做特殊的处理,很容易就出现数据不一致的情况。
通常情况下,我们可以用加锁的方式来保证线程安全,通过对共享资源 (也就是要读取的数据) 的加上"隔离的锁",使得多个线程执行的时候也不会互相影响,而悲观锁和乐观锁正是并发控制中较为常用的技术手段。
乐观锁和悲观锁
什么是悲观锁?什么是乐观锁?其实从字面上就可以区分出两者的区别,通俗点说,
悲观锁
悲观锁就好像一个有迫害妄想症的患者,总是假设最坏的情况,每次拿数据的时候都以为别人会修改,所以每次拿数据的时候都会上锁,直到整个数据处理过程结束,其他的线程如果要拿数据就必须等当前的锁被释放后才能操作。
使用案例
悲观锁的使用场景并不少见,数据库很多地方就用到了这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁,悲观锁的实现往往依靠数据库本身的锁功能实现。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知识的文章出来,欢迎大家多来踩踩!
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
冷月手撕408之计算机网络(2)-计算机网络概述
计算机网络概述主要是一些了解的知识,大家不用花太多的时间在这上面。其中计网的概念和计网的功能相对最重要,大家要记住。这一节一般是选择题考点。 主要的重点冷月做出了标识,知识点如下图(pdf版或xmind源文件请关注公众号:学长冷月,回复计算机网络)。 计算机网络概念 要想学习计算机网络,首先我们要知道计算机网络是什么。计算机网络主要由一些通用、可编程的硬件互联而成,通过这些硬件,可以传送不同类型的数据,并且可以支持广泛和日益增长的应用。通俗来讲,计算机网络就是一些互连、自治的计算机系统的集合,并且这个集合还可以随着技术的发展而动态增加或减少。 互联网的发展历史 在计算机出现后,我们更多使用计算机的时候是联系他人,比如QQ、微信、打网游,而我们不是自己一个人玩。所以我们要怎么才能和其他小伙伴一起愉快的玩耍呢,这就促使了互联网的诞生。 互联网的发展历史主要分为三个阶段,如下图所示 第一个阶段 ARPAnet ARPANET是美国高级研究计划署(Advanced Research Project Agency)的简称。是世界上最早的计算机网络,它是美国国防部高级研究计划局,信息处理处开发的世...
- 下一篇
电感最重要的公式
大家好,今天来给大家讲一个与电感有关的公式,也是我认为关于电感最重要的公式。这个公式是什么呢?就是下面这个: 这个公式来源于电感值本身特性。 为什么说这个公式是最重要的呢?因为它说明了电感的很多特性。比如, 电感电流不能突变 电感的储能大小 电感的电流与电压的相位关系 还有电感的阻抗为什么是jwL 电感电流不能突变 电感电流为什么不能突变呢?来看这个公式,U等于负的L乘以di比dt。Di比dt是指电流的变化率,电流突变,意味着di比dt无限大,会导致产生无限大的电压。尽管在实际电路中绝对的电流突变不存在,或多或少都会有时间,因此产生的电压总不会真的无穷大,但是是真的能产生很大的电压,高于电源电压都是可能会出现的。这种意外的高压会损坏器件。所以我们在一些感性的开关电路中,需要对感性器件留一个放电回路,避免产生高压。例如开关电源,继电器电路。通常是通过RC电路进行缓冲,也有的用二极管,TVS等。 电感储能公式 电感的储能也是由这个公式推导出来的,下面是推导过程,需要一些微积分的知识,感兴趣的可以看一下过程,不感兴趣的记下这个结果,电感储能为1/2LI^2,单位是焦耳。 电感在t时间内,电...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装