zoukankan      html  css  js  c++  java
  • 面试必问系列:悲观锁和乐观锁的那些事儿

    本文转自(https://segmentfault.com/a/1190000023866733),版权归原作者所有,仅备份

     

    程序安全

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

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

    乐观锁和悲观锁

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

    悲观锁

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

    使用案例

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

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

    select * from db_stock where goods_id = 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 where goods_id and stock_num 0

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

    最后

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

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

  • 相关阅读:
    SDUT 2143 图结构练习——最短路径 SPFA模板,方便以后用。。 Anti
    SDUT ACM 1002 Biorhythms 中国剩余定理 Anti
    nyist OJ 119 士兵杀敌(三) RMQ问题 Anti
    SDUT ACM 2157 Greatest Number Anti
    SDUT ACM 2622 最短路径 二维SPFA启蒙题。。 Anti
    二叉索引树 区间信息的维护与查询 Anti
    SDUT ACM 2600 子节点计数 Anti
    UVA 1428 Ping pong 二叉索引树标准用法 Anti
    2010圣诞Google首页效果
    Object
  • 原文地址:https://www.cnblogs.com/JAYIT/p/14081778.html
Copyright © 2011-2022 走看看