一、锁的分类
1)乐观锁 vs 悲观锁
乐观锁:多个线程竞争一把锁的概率会很低
悲观锁:世界大概率是出问题的 多个线程竞争一把锁的概率会很高,会付出更多的成本来进行锁冲突的处理
两种方法没有优劣之分,要根据具体场景进行使用
2)读写锁
把加锁操作分成了两种。
a)读锁
b)写锁
读锁和读锁之间是没有互斥的(不存在锁竞争),读锁和写锁/写锁和写锁 之间才进行互斥
如果某个场景下“一写多读”,使用读写锁效率就很高。
3)重量级锁 vs 轻量级锁
加锁需要保证原子性 原子性功能来源自硬件(硬件提供了相关的原子操作的指令,操作系统封装一下成为原子操作的接口,应用程序才能使用这样的操作)
在加锁的过程中,如果整个加锁逻辑都是依赖于操作系统内核,此时就是重量级锁(代码在内核中的执行开销会比较大)对应的,如果大部分操作都是用户自己完成,少数操作由内核完成,这种就是轻量级锁。
4)挂起等待锁 vs 自旋锁
挂起等待锁 表示 当前获取锁失败之后,对应的线程就要在 内核中挂起等待(放弃 CPU,进入等待队列)
需要在锁被释放之后由操作系统唤醒【通常都是重量级锁】
自旋锁表示在获取锁失败后,不会立刻放弃 CPU,而是快速频繁的再次询问锁的持有状态,一旦锁被释放了,就能立刻获取到锁【通常都是轻量级锁】
自旋锁的效率更高,但是会浪费一些 CPU 资源(自旋过程相当于 CPU 在空转)
一旦线程挂起,下次啥时候能被调度上,就不可预期了(时间可能很久,能够达到ms级别,极端情况下可能是 s 级)
Windows/Linux 这样的系统,调度过程都是没那么快的~~~调度时间不可预期】
5)公平锁 vs 非公平锁
如果多个线程都在等待一把锁的释放,当锁释放之后,恰好又来了一个新的线程也要获取锁。
公平锁:能保证之前先来的线程优先获取锁
非公平锁:新来的线程直接 获取到锁,之前的线程还得接着等
要想实现公平锁,就需要付出一些额外的代价
6)可重入锁
一个线程针对一把锁,连续加锁两次,不会死锁,这种就是可重入锁。
当锁的持有这正好就是新的锁的申请者,此时就特殊处理下让加锁操作成功即可。
死锁的典型场景:
1.一个线程针对一把锁连续加两次
2.两个线程针对两把锁分别加锁
3.N个线程针对 N 把锁分别加锁
二、锁优化的策略
锁优化就是一些提高锁的效率的策略,下面以synchronized为例,来介绍优化操作。
1.锁消除: 编译器 + JVM 会根据代码运行的情况智能判定当前的锁是否必要,如果不必要,就直接把加锁的代码忽略。
2.偏向锁:第一个尝试加锁的线程,不会真的加锁,而是进入偏向锁状态(很轻量的标记),直到其他线程也来竞争这把锁,才会取消偏向锁状态,真正进行加锁
1和2的优化思路相同:能不加锁就不加锁
3.自旋锁:真的有多个线程竞争锁的时候,偏向锁状态被消除,此时线程使用自旋锁的方式来尝试进行获取锁。自旋锁能保证让其他想竞争锁的状态尽快获取到锁,付出了一定的CPU 资源
4.膨胀锁 (无奈之举,严格上说,不能算优化)
当锁竞争更加激烈,此时就会从自旋锁状态膨胀成重量级锁(挂起等待锁)
5.锁粗化
如果一段逻辑中,需要多次加锁解锁,并且加锁解锁的时候没有其他线程来竞争,此时就会把多组加锁操作,合并到一起。
粗化就是把 多组加锁解锁 操作合并成一组。每次加锁解锁操作,都有开销,减少加锁的次数,就能提高效率了。



