悲观锁与乐观锁

600 阅读4分钟

准备从以下脑图中的内容来总结悲观锁与乐观锁

含义

什么是悲观锁与乐观锁?

悲观锁:悲观锁就是在一个线程在拿到数据后,每次都做出最坏的打算,总以为别人会修改当前数据,所以对当前收据进行加锁,只有在当前线程释放锁后,其它线程才可以使用数据。期间其它线程一直被阻塞。

乐观锁:乐观锁每次都是做出最好的打算,认为其它线程不会修改当前数据,当前线程只有在修改数据的时候才会去进行一下对比,如果对比成功,就进行修改,否则进行无限重试。

悲观锁与乐观锁只是一种思想,具体的实现方式有好多种,但是实现的时候要掌握好锁的粒度,一旦掌握不好,就能在性能方面就有很大的损失。

实现原理

悲观锁的实现原理

悲观锁的实现就是对一大块代码进行加锁,或者对整个方法进行加锁,锁定粒度可以说相当大了,所有目前很多的高并发场景下很少使用悲观锁。

乐观锁的实现原理

乐观锁的实现原理有两种,一种是CAS(Compare And Swap)实现,另一种是版本号机制实现。相对于悲观锁而言,乐观锁的使用次数很多,备受欢迎。

  • CAS操作包括了三个操作数(需要读写的操作位置,进行比较的预期值、拟写入的新值),在进行更新操作之前,会从内存中读出变量,放在预期值中,然后在执行更新操作的时候,会比较预期值和现在内存中的值是否相等,如果相等则写入,不相等则证明此值在期间被修改过,则放弃此次操作,进行重试
  • 版本号机制,版本号机制就是在变量前加一个字段,即version,每次进行操作之前,把当前的版本读取出来,然后在进行修改之前判断当前的版本号和当初读取出来的版本号是否相同,相同则进行修改,不相同则放弃此次操作,进行重试

基本应用

悲观锁和乐观锁的应用都很广泛,悲观锁的典型应用就是在java中的synchronized,还有就是在传统的关系型数据库中的表锁、行锁、读锁、写锁(独占锁)。

乐观锁的典型应用就是在ConCurrentHashMap中,ConCurrentHashMap中采用乐观锁 + 悲观锁实现的,还有就是在java中的java.util.concurrent.atomic包下的类。

另外就是悲观锁适合写情况较多的场景,这样可以有效较少冲突与碰撞,另外要注意锁的粒度。而乐观锁适用于读情况较多的场景,例如ConCurrentHashMap,一旦写的操作较多,线程就可一直在重试写操作,非常耗费CPU的性能。

存在的问题

悲观锁存在的问题:

  • 读写速度慢

    因为悲观锁是成块儿加锁的,当前线程在操作一个共享变量的时候,如果其他线程也想操作这个变量,那么必须等当前线程释放锁后,其它线程才可以进行读写。

  • 耗费CPU资源

    这又是如何说起,因为成块的加锁导致线程之间的切换相当频繁,而线程之间的切换会导致操作系统从用户态转化为内核态,这是相当消耗CPU资源的。

CAS存在的问题:

  • ABA问题

    ABA问题是CAS的典型问题,例如当其中的一个线程读取到了内存值为1,然后进行值的修改,在修改的过程中其它线程也对此值进行了修改,比如修成了2,又来了一个线程将此值又修改回了1,但是第一个线程进行比较的时候误认为值没有被修改,然后就成功的把此值给更行了。

  • 重试

    当CAS读取出来的内存值被修改了,那么当前线程是无法对此值进行修改的,只能一直重试,直到重试成功。也是非常耗费CPU资源的。

  • 单值操作

    从内存中读取出来的值和待比较的值只能为单值,就是单一一个值,如果想要进行多值更改,就只能将这些值封装为一个对象或者结构体。

  • 使用限制

    我们使用CAS只在使用java.util.concurrent.atomic包下的api,不能自己直接操作,因为CAS中原子性(查询、比较、修改)是靠底层硬件(CPU)进行维护的,普通用户根本无法操作。

针对CAS存在的问题解决办法

  • ABA问题

    可以加入版本限制,当修改后,令版本号自加,比较的时候要同时比较版本号和期望值

  • 重试

    引入退出等待的概念,当重试一定的次数后,退出重试,让出CPU

  • 单值操作

    对多个值进行一次封装,封装成对象或者结构体