目录
1. 乐观锁和悲观锁
2. 轻量级锁与重量级锁
3. 自旋锁和挂起等待锁
4. 互斥锁和读写锁
5. 可重入锁与不可重入锁
6. 死锁
6.1 死锁的必要条件
6.2 如何避免死锁
7. 公平锁和非公平锁
8. Synchronized原理及加锁过程
8.1 Synchronized 小结
8.2 加锁工作过程
8.2.1 偏向锁
8.2.2 轻量级锁
8.2.3 重量级锁
9. 锁优化
9.1 锁消除
9.2 锁粗化
1. 乐观锁和悲观锁
锁的实现者,预测接下来的冲突概率(锁竞争)大还是不大,然后根据这个冲突的概率,来决定接下来应该怎么做.
乐观锁:预测接下来的冲突概率不大
悲观锁:预测接下来的冲突概率很大
通常来说~~悲观锁一般要做的工作更多一些,效率会更低一些~(并不绝对)
乐观锁做的工作会更少一点,效率更高一点.
2. 轻量级锁与重量级锁
轻量级锁加锁解锁过程更快更高效.
重量级锁加锁解锁过程更慢,更低效
和乐观悲观虽然不是一回事,但是确实有一定的重合~
一个乐观锁很可能也是一个轻量级锁(不绝对)
一个悲观锁很可能也是一个重量级锁(不绝对)
3. 自旋锁和挂起等待锁
自旋锁是轻量级锁的一种典型实现
挂起等待锁是重量级锁的一种典型实现举例:追女朋友
自旋锁:小白像一个女孩表白失败,被拒绝(因为这女生有男朋友(被加锁成功)).但是一直没有放弃,每天都在追求,时刻不停歇.(这就是一个不停的等待锁的过程,)当这个女生分手了(解锁),小白就会第一时间得到通知,竞争到锁的机会就很大.
挂起等待锁:小白选择了另一种方式,小白暂时先不去追这个女生,等过一段时间来问问女生是否分手(解锁),此时就是不能立马得到通知,很有可能之前的锁解除了,但是小白没有拿到,被别人拿到了.优点就是这段时间小白不用每天都问是否分手,可以集中注意力做一些别的事情,比如学习Java.哈哈哈.
补充:
针对上述三组策略,synchronized这把锁属于那种呢?
synchronized既是悲观锁,也是乐观锁,既是轻量级锁,也是重量级锁,轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现~~
如果锁冲突不激烈以轻量级锁/乐观锁的状态运行
如果锁冲突激烈以重量级锁/悲观锁的状态运行
4. 互斥锁和读写锁
synchronized,是互斥锁~~ 加锁,就只是单纯的加锁,没有更细化的区分了~~
像synchronized只有两个操作:
1.进入代码块加锁
2.出了代码块解锁~~
一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.
- 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
- 两个线程都要写一个数据, 有线程安全问题.
- 一个线程读另外一个线程写, 也有线程安全问题.
读写锁: 是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
- ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
- ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁.
读写锁约定:
- 读加锁和读加锁之间, 不互斥.(没锁竞争)
- 写加锁和写加锁之间, 互斥.(有锁竞争)
- 读加锁和写加锁之间, 互斥. (有锁竞争)
生活场景应用
读写锁特别适合于 "频繁读, 不频繁写" 的场景中. (这样的场景其实也是非常广泛存在的).
比如教务系统
5. 可重入锁与不可重入锁
可重入锁.:如果一个锁在一个线程中,连续对该锁咔咔加锁两次不死锁,就叫做可重入锁.即允许同一个线程多次获取同一把锁。
不可重入锁:如果死锁了,就叫不可重入锁~~
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的.
6. 死锁
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
死锁的情况:
1. 一个线程,一把锁,可重入锁不会发生死锁,不可重入锁发生死锁
2. 两个线程,即使是可重入锁也有可能会发生死锁
3. N个线程M把锁就非常容易死锁.
6.1 死锁的必要条件
死锁的必要条件:(缺一不可)
- 1.互斥使用.一个线程拿到一把锁之后,另一个线程不能使用.(锁的基本特点)
- 2.不可抢占.一个线程拿到锁,只能自己主动释放,不能是被其他线程强行占有~~[挖墙脚是不行滴](锁的基本特点)
- 3.请求和保持."吃着碗里的,惦记锅里的”追到了1号女神之后,又对2号女神跃跃欲试,但是此时是绝不会放弃1号女神的~~(代码的特点)
- 4.循环等待.上面例子中的情况,逻辑依赖循环的.“钥匙锁车里了,车钥匙锁家里了”(代码的特点)
6.2 如何避免死锁
死锁是个比较严重的bug.实践中如何避免出现死锁呢?
一个简单有效的办法:破解循环等待这个条件~~
针对锁进行编号,如果需要同时获取多把锁,约定加锁顺序,务必是先对小的编号加锁后对大的编号加锁~~如果此时约定,先加锁小的编号,后加锁大的编号,此时只要所有线程都遵守这个顺序就行了!
7. 公平锁和非公平锁
假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后C 也尝试获取锁, C 也获取失败, 也阻塞等待.当线程 A 释放锁的时候, 会发生啥呢?
公平锁: 遵守 "先来后到". B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
非公平锁: 不遵守 "先来后到". B 和 C 都有可能获取到锁.操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构(队列), 来记录线程们的先后顺序.公平锁和非公平锁没有好坏之分, 关键还是看适用场景.
synchronized 是非公平锁.
8. Synchronized原理及加锁过程
8.1 Synchronized 小结
8.2 加锁工作过程
8.2.1 偏向锁
1) 偏向锁
第一个尝试加锁的线程, 优先进入偏向锁状态.偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程.如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销),如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.偏向锁本质上相当于 "延迟加锁" . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.
8.2.2 轻量级锁
2) 轻量级锁
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).此处的轻量级锁就是通过 CAS 来实现.(后续会总结这个CAS(先理解为比较内存和寄存器的值,相同就更新忙不相同就修改为内存的值之后再对数据进行操作))
- 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
- 如果更新成功, 则认为加锁成功
- 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.
因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.也就是所谓的 "自适应"
8.2.3 重量级锁
3) 重量级锁
如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁此处的重量级锁就是指用到内核提供的 mutex .
- 执行加锁操作, 先进入内核态.
- 在内核态判定当前锁是否已经被占用
- 如果该锁没有占用, 则加锁成功, 并切换回用户态.
- 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
- 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁.
9. 锁优化
9.1 锁消除
编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.
什么是 "锁消除"
有些应用程序的代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer))StringBuffer sb = new StringBuffer(); sb.append("a"); sb.append("b"); sb.append("c"); sb.append("d");
此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加
锁解锁操作是没有必要的, 白白浪费了一些资源开销.补充:StringBuffer相对于StringBuilder是相对线程安全的,因为StringBuffer把关键方法都加上了Synchronized关键字,但是不是绝对线程安全,看代码怎么写.
9.2 锁粗化
锁粗化
一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.举例: