目录
1.常见的锁策略
1.乐观锁 vs 悲观锁
- 悲观锁: 一开始就会上锁
- 乐观锁: 假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做
2.读写锁
- 多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
- 读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
- ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
- ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁.
其中,
- 读加锁和读加锁之间, 不互斥.
- 写加锁和写加锁之间, 互斥.
- 读加锁和写加锁之间, 互斥.
注意, 只要是涉及到 "互斥", 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多
久了. 因此尽可能减少 "互斥" 的机会, 就是提高效率的重要途径
读写锁特别适合于 "频繁读, 不频繁写" 的场景
3.重量级锁 vs 轻量级锁
锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
- CPU 提供了 "原子操作指令".
- 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
- JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.
重量级锁: 加锁机制重度依赖了 OS 提供了 mutex
- 大量的内核态用户态切换
- 很容易引发线程的调度
重量级锁过程
- 执行加锁操作, 先进入内核态.
- 在内核态判定当前锁是否已经被占用
- 如果该锁没有占用, 则加锁成功, 并切换回用户态.
- 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
- 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒 这个线程, 尝试重新获取锁.
这两个操作, 成本比较高. 一旦涉及到用户态和内核态的切换, 就意味着 "沧海桑田".
轻量级锁: 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex.
.
- 少量的内核态用户态切换.
- 不太容易引发线程调度
此处的轻量级锁就是通过 CAS 来实现.
- 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
- 如果更新成功, 则认为加锁成功
- 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.
因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.
也就是所谓的 "自适应"
4.自旋锁(Spin Lock)
- 如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.
- 自旋锁是一种典型的 轻量级锁 的实现方式.
- 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
- 缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是不消耗 CPU 的).
5.公平锁 vs 非公平锁
- 公平锁: 遵守 "先来后到". B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
- 非公平锁: 不遵守 "先来后到". B 和 C 都有可能获取到锁.
- 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要 想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
- 公平锁和非公平锁没有好坏之分, 关键还是看适用场景.
6.可重入锁 vs 不可重入锁
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入
锁(因为这个原因可重入锁也叫做递归锁)。
- Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括
- synchronized关键字锁都是可重入的。
- 而 Linux 系统提供的 mutex 是不可重入锁.
7.Synchronized实现了哪些锁策略?
1.是乐观锁也是悲观锁
开始没有线程竞争的时候无锁,当有第一个线程来使用的时候加偏向锁,偏向锁并不是真正的加锁,只是给对象头做一个偏向锁的标记,记录这个锁属于哪个线程,如果后续没有其他线程来争抢锁,那就不用真正的加锁,(避免了加锁解锁的系统开销)并记录当前线程的版本号,当出现两个以上的线程竞争时变为轻量级锁,当越来越多的线程参与到竞争中,自旋不能快速获取到锁状态,此时就会变为重量级锁
2.既是轻量级锁也是重量级锁
轻量级锁是基于自旋锁实现的,重量级锁是基于挂起等待锁实现的
3.是普通互斥锁
4.是非公平锁
5.是可重入锁
6.既是自旋锁也是挂起等待锁
8.CAS(Compare And Swap)
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号
9.基于CAS的应用,原子类
两个线程通过CAS同时对一个共享变量做自增,通过不停的自旋检查预期值来保证了线程安全
while循环是在应用层执行的,也就是用户态锁比内核态的锁效率要高很多
Compare And Swap 是CPU中的一条指令,可以完成CAS的整个操作(比较并交换),简而言之,是因为硬件予以了支持,软件层面才能做到
10.CAS实现自旋锁
11.CAS的ABA问题
1.ABA问题
在上述过程中,两个A校验的时候都可以通过,但两个A却并非同一个A,
如果CAS中出现ABA问题,在真实的业务中可能会造成比较大的影响
2.解决ABA问题
给预期值加一个版本号,在做CAS操作的同时更新预期值的版本号,版本号只增不减
CAS 操作在读取旧值的同时, 也要读取版本号,真正修改的时候
- 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
- 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)
12.加锁工作过程
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级