mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
86 lines
6.1 KiB
Markdown
86 lines
6.1 KiB
Markdown
# 深入理解各种锁
|
||
|
||
## 乐观锁、悲观锁
|
||
|
||
乐观锁对应于现实生活中乐观的人,思考事情总往好的方向发展;悲观锁对应于现实生活悲观的人,思考事情总往坏的方向发展。不同性格的人都有优缺点,不能抛开场景说一种人好而另一种人不好。
|
||
|
||
乐观锁和悲观锁是一种广义上的概念,体现了看待线程同步问题的不同角度,在 iOS、Java、数据库中都有此概念。
|
||
|
||
|
||
|
||
### 悲观锁
|
||
|
||
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定会有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
|
||
这种线程一旦得到锁,其他需要锁的线程就挂起。共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程。传统的关系型数据库就用到很多悲观锁这种几只,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
|
||
|
||
### 乐观锁
|
||
|
||
乐观锁认为自己在使用数据的时候不会有别的线程来修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果数据已经被别的线程更新,则根据不同方式执行不同操作(例如报错或者自动重试)。
|
||
|
||
可以根据版本号机制和 CAS 算法实现。
|
||
|
||
乐观锁适合多读少写的应用类型或者场景,即冲突真的很少发生的场景,这样省去了锁的开销,加大了系统的吞吐量。但是如果多写少读的情况,一般会经常发生冲突,这样会导致上层应用层不断 retry,这样反而降低了性能,所以一般建议多写的场景下使用悲观锁比较合适。
|
||
|
||
|
||

|
||
|
||
|
||
|
||
### 乐观锁常见的实现方式
|
||
|
||
乐观锁一般使用版本号机制或者 CAS 算法实现。
|
||
|
||
|
||
|
||
#### 1. 版本号机制
|
||
|
||
在数据表增加一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时, version 值加1。当线程1更新数据的时候,先拿到数据并读取出 version 值,修改完数据进行提交更新的时候时,若读取出的 version 值为当前数据库中 version 值相等时才更新,否则重试更新操作,直到更新成功。
|
||
|
||
举个例子:
|
||
假设数据库中账户信息表有一个字段 version,值为1;当前账户余额为100。当需要对账户信息表进行更新的时候,需要读取 version 字段,以及账户余额信息
|
||
|
||
- 用户 A 读出数据:version = 1,balance = 100。从账户余额中扣除 50, balacne = 50
|
||
|
||
- 用户 B 比用户 A 刚刚晚一点点时间,读出数据 :version = 1, balance = 100。从账户余额中扣除 20,balance = 80
|
||
|
||
- 用户 A 完成修改操作,需要提交更新,但是在更新之前会先判断数据库中的版本号 version 值和自己读取到的 version 值是否一致,如果一致,则将版本号 version 字段的值加1(version = 2),连同账户扣除后的余额(balance = 50),提交到数据库服务器执行更新操作,此时由于提交数据中版本号大于数据库记录中的版本,则数据被更新,数据库记录 version = 2
|
||
|
||
- 用户 B 完成修改操作,同样在更新之前先读取数据库中的版本号 version 值和自己读取到的 version 值是否一致,但此时发现自己读取到的 version = 1,数据库中的 version = 2,很显然不满足“当前最后更新的版本号 version 与操作员第一次读取到的版本号 version 相等”的乐观锁策略,因此用户 B 的提交被驳回。
|
||
|
||
这样,就避免了用户 B 基于 version = 1 的旧数据修改的结果覆盖用户 A 操作的结果,
|
||
|
||
#### 2. CAS 算法
|
||
|
||
**compare and swap(比较与交换)** ,是一种有名的**无锁算法**。 无锁编程,即在不实用锁的情况下实现多线程之间的数据同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫做**非阻塞同步(Non-blocking Synchorization)**。CAS 算法涉及到的三个操作数
|
||
|
||
- 需要读写的内存值 V
|
||
- 进行比较的值 A
|
||
- 拟写入的新值 B
|
||
|
||
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V,否则不会执行任何操作。比较和替换是一个原则操作。一般情况下是一个自旋操作,即不断的重试
|
||
|
||
|
||
|
||
### 乐观锁的缺点
|
||
|
||
1. ABA 问题
|
||
如果一个变量 V 初次读取的时候的值为 A,并且在准备赋值的时候检查到变量 V 的值仍然是 A,那么可以说是 V 的值从来没被其他线程修改吗?很明显不能,因为有可能变量 V 的值,从 A 变到 B,然后又改回到 A,那么 CAS 的标准就会认为变量 V 从来没被修改过,这类问题被成为 CAS 的 **ABA** 问题。
|
||
2. 循环时间长、开销大
|
||
自旋 CAS (也就是不成功就一直循环操作直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。
|
||
3. 只能保证一个共享变量的原则操作
|
||
CAS 只对单个变量共享有效,当操作涉及到多个共享变量时,CSA 无效。
|
||
|
||
|
||
|
||
### CAS 与 synchorized 的使用场景
|
||
|
||
一般来说, CAS 适用于乐观锁,多读少写场景,冲突一般较少,则自旋操作的情况非常少,不会消耗 CPU,该场景合适。synchorized 使用悲观锁,多写少读场景,冲突一般较多。
|
||
|
||
1. 对于资源竞争比较少(线程冲突较轻)的情况,如果使用 synchorized 同步锁进行线程阻塞和唤醒切换以及用户内核态间的切换操作额外浪费 CPU 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋的几率较少,因此可以获得更高的性能。
|
||
2. 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的几率会比较大,从而浪费更多的 CPU 资源。效率低于 synchorized
|
||
|
||
|
||
|
||
|
||
### 参考资料
|
||
- [不可不说的Java“锁”事](https://tech.meituan.com/2018/11/15/java-lock.html) |