• 乐观锁与悲观锁


    [注]文档为学习整理,若有需要补充,请各位看官多多指点。

    基本概念

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

    • 乐观锁:乐观锁在操作数据时非常乐观,认为鄙人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

    • 悲观锁:悲观锁在操作数据时比较悲观,认为鄙人会同时修改数据。因此操作数据时直接把数据所著,直到操作完成后才释放锁;上锁期间其他人不能修改数据。

    实现方式

    乐观锁和悲观锁是两种思想,它们的使用非常广泛,不局限于某种编程语言或数据库。

    悲观锁的实现方式是加锁,加锁既可以是对代码块加锁,也可以是对数据加锁。

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

    CAS(Compare And Swap)

    CAS 操作包括了3个操作数:

    • 需要读写的内存位置 (V)

    • 进行比较的预期值 (A)

    • 拟写入的新值 (B)

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

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

    下面以 Golang 中的自增操作为例,看一下悲观锁和 CAS 分别是如何保证线程安全的。

     package main
     
     import (
      "fmt"
      "sync"
      "sync/atomic"
     )
     
     var (
      value1 = 0
      value2 = 0
      value3 uint32 = 0
     )
     
     func main() {
      wg := sync.WaitGroup{}
      lock := sync.Mutex{}
      for i := 0; i < 1000; i++ {
      wg.Add(1)
     
      go func() {
      defer wg.Done()
      value1++
      lock.Lock()
      value2++
      lock.Unlock()
      atomic.AddUint32(&value3, 1)
      }()
      }
      wg.Wait()
      fmt.Println("value1: ", value1)
      fmt.Println("value2: ", value2)
      fmt.Println("value3: ", value3)
     }

    在代码实例中:value1并没有进行任何线程安全方面的保护,value2使用了悲观锁(sync),value3使用了乐观锁(atomic)。运行程序,使用1000个协程同时对value1、value2、value3进行加1操作,可以发现:value2和value3的值总是等于1000,而value1的值尝尝小于1000。

    版本号机制

    除了CAS,版本号机制也可以用来实现乐观锁。版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一直,如果一致才进行操作。

    需要注意的是,这里使用了版本号作为判断数据变化的标记,实际上可以根据实际情况选用其他能够标记数据版本的字段,如时间戳等。

    是否加锁

    乐观锁本身时不加锁的,只是在更新时判断一下数据是否被其他线程更新了。

    优缺点和适用场景

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

    功能限制

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

    例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而sync则可以通过对整个代码加锁来处理。再比如版本号机制,如果查询的时候针对数据A,而更新的时候针对数据B,也很难通过简单的版本号来实现乐观锁。

    竞争激烈从程度

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

    • 当竞争不激烈(出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,并且加锁和释放锁都需要消耗额外的资源。

    • 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

    ABA问题

    假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:

    1. 线程1读取内存中数据为A;

    2. 线程2将该数据改为B;

    3. 线程2将该数据改为A;

    4. 线程1对数据进行CAS操作;

    在第4步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。

    在有些场景下,例如数字的增减,ABA似乎没有什么危害。但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已经发生了变化。

    对于ABA问题,比较有效的方案是引入版本号,内存中的值没发生一次变化,版本号都加1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。

    参考资料

  • 相关阅读:
    Go语言基础之字符串遍历
    Go语言基础之range
    Go语言的for循环
    Go语言基础之反射
    Go语言基础之接口
    Linux编程简介
    如何使用gcc编译器
    ADS的使用
    bvp4c--语法
    어느 도시 보유 하 면 사랑 이다(事態が発生すれば、ある都市の恋はしません)【Si les villes un amour】{If have love in a city}
  • 原文地址:https://www.cnblogs.com/xiaohexiansheng/p/16213711.html
Copyright © 2020-2023  润新知