| 锁策略 | 说明 | 实现 |
|---|---|---|
| 乐观锁 | 认为锁冲突是小概率事件,假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。 | 引入一个版本号,借助版本号识别出当前的数据访问是否冲突,如果冲突则将数据更新。 |
| 悲观锁 | 认为锁冲突是大概率事件,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。 | 先加锁(比如借助操作系统提供的 mutex),获取到锁再操作数据们获取不到就等待。 |
乐观锁和悲观锁并无高下之分,主要还是看应用场景。
synchronized 既是一个乐观锁,又是一个悲观锁。能够根据实际场景,自动进行适应。
读写锁就是把读操作和写操作分别加锁,适用于读多写少的场景中,Java 标准库提供了 ReentrantReadWriteLock 类,该类实现了读写锁:
| 锁策略 | 实现类 | 补充 |
|---|---|---|
| 读锁 | ReentrantReadWriteLock.ReadLock | 这个对象提供了 lock/unlock 方法进行加锁和解锁。 |
| 写锁 | ReentrantReadWriteLock.WriteLock | 这个对象提供了 lock/unlock 方法进行加锁和解锁。 |
其中读锁和写锁之间的关系如下:
synchronized 不是读写锁。
锁的核心特性原子性,追根溯源是 CPU 这样的硬件设备提供的:
mutex 互斥锁| 锁策略 | 说明 | 特点 |
|---|---|---|
| 重量级锁 | 加锁机制重度依赖了 OS 提供的 mutex | 涉及到了大量的内核态和用户态的切换,易引发线程的调度。 |
| 轻量级锁 | 加锁机制尽可能不使用 mutex,而尽量在用户态代码完成,实在搞不定了才使用mutex | 很少会涉及内核态和用户态的切换,不容易引发线程调度 |
| 锁策略 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 自旋锁 | 如果获取锁失败,就会立即再次获取锁,循环下曲直到获取到锁为止 | 没有放弃 CPU,不涉及到线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁 | 如果锁被其他线程持有的时间比较久,那就就会持续消耗 CPU 资源 |
| 挂起等待锁 | 如果获取锁失败,就会挂起等待,会放弃 CPU,进入内核的阻塞队列,直到锁被释放且 CPU 调度到该线程 | 节省 CPU 资源 | 速度比较慢,一旦锁释放不能够第一时间直到 |
如果有 A、B、C 三个线程,A 线程在获取到锁后,B 紧接着获取锁但失败了,C 最后也获取锁也失败了。当 A 释放锁后,如果按照先来后到的顺序获取锁,即 B 先获取锁,则是一个公平锁。如果不遵循先来后到,B 和 C 都有可能获取锁,则是一个非公平锁,
| 锁策略 | 说明 |
|---|---|
| 公平锁 | 遵循先来后多 |
| 非公平锁 | 不遵循先来后到,随即调度 |
| 锁策略 | 说明 | 实现 |
|---|---|---|
| 可重入锁 | 允许同一个线程多次获取同一把锁 | 在锁中记录锁持有的线程的身份,以及一个计数器(记录加锁的次数),如果当前加锁的线程就是持有锁的线程,则直接计数自增。 |
| 不可重入锁 | 不允许同一个线程多次获取同一把锁 |