1. 乐观锁VS悲观锁
锁得实现者,预测接下来锁冲突的概率(同个线程针对同一个对象加锁,产生阻塞等待)大不大,根据这个冲突的概率,来决定接下来该怎么做~
- 乐观锁:看名字就是比较乐观,预测冲突概率不大,所以做的工作会比较少,效率会高一点(并不绝对)
- 悲观锁:就是预测冲突概率比较大,所做的工作比较多,效率会低一点(并不绝对)
乐观锁:假设数据一般情况下不会发生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则返回用户错误的信息,让用户决定如何去做
悲观锁:总是假设最坏情况,每次对数据都会进行修改操作,所以在每次拿数据的时候都会上锁,这样其他线程想操作这个数据就会阻塞等待
2. 轻量级锁VS重量级锁
synchronized 并不仅仅是对 mutex 进行封装,内部还做了很多其他的工作
synchronized 开始是一个轻量级锁,如果锁冲突比较严重,就会 变成重量级锁
- 轻量级锁:加锁解锁,过程更快更高效,加锁机制尽可能不使用mutex,而是在用户态代码完成,实在搞不定,再使用mutex
- 少量的内核用户态切换
- 不太容易引发线程的调度
- 重量级锁:加锁解锁,过程更慢更低效,加锁机制重度依赖OS提供的mutex
- 大量的内核用户态切换
- 很容易引发线程的调度
3. 自旋锁VS挂机等待锁
自旋锁(轻量级锁的一种典型实现)
- 按照之前的方式,线程在抢锁失败后进入阻塞状态,放弃CPU,需要很久才能再次调度,然后实际上,在大多数情况下,虽然抢锁失败,但过不了多久,锁就会被释放。所以就没必要放弃CPU,这个时候就可以使用自旋锁来处理这种情况。
- 优点:如果获取锁失败后,就会一直尝试获取,无限循环,一直等到获取锁位置!一旦锁被其他线程释放,就会第一时间获取到锁!
- 缺点:会造成忙等,消耗CPU资源
挂起等待锁(重量级锁的一种典型实现)
- 挂起等待锁,当抢锁失败后,就放弃了CPU,进入到等待队列,这个时候如果其他线程释放锁之后,就会被唤醒,但是不能第一时间拿到资源的,可能就会需要很久才能获取到锁!
举两个例子:
例子1(自旋锁):假设你向女神表白,但是你被发了好人卡~,但是你仍旧每天锲而不舍的,向女神发送早安晚安,一旦哪一天,女神和男朋友分手了,此时你就有很大机会,把锁给加上!
例子2(挂起等待所):表白失败后,你就不找女神了,或许在女神分手后某一天,想到了你,就会主动来找你,这个时候你的机会也来了,但是不一样你就是找的第一个~
针对上述三组策略:提出个疑问? synchronized 属于哪一种
synchronized 既是悲观锁,也是乐观锁;既是轻量级锁,也是重量级锁;轻量级锁基于自旋锁实现,重量级锁基于挂起等待锁实现~
具体synchronized 会根据当前锁竞争的激烈程度,自适应
4. 互斥锁VS读写锁
synchronized就是互斥锁,加锁就是单纯的加锁,没有更细化的区分。
但是读写锁,能够把 读 和 写 两种加锁区分开来
在多线程中,数据的读取不会产生线程安全的问题。但是数据的写入互相之间和读之间都需要进行互斥。 此时考虑到一个问题,如果两种情况下都使用同一个锁,就会产生极大的性能消耗,所以读写锁就产生了!
一个线程对于数据的访问,就要是两种:读操作和写操作
- 两个线程都只是进行读操作,没有线程安全问题
- 两个线程都要进行写操作,就会有线程安全问题
- 一个线程读另一个线程写,就会有线程安全问题
所以针对上面读和写操作区别,Java标准库提供了 ReentrantReadWriteLock 类,实现了读写锁
- ReentrantReadWriteLock.ReadLock 类 表示 一个读锁,这个对象提供了 lock/ unlock 进行加锁解锁
- ReentrantReadWriteLock.WriteLock 类 表示 一个写锁,这个对象也提供了 lock/unlock 方法进行加锁解锁
其中:
- 读加锁和读加锁之间,不互斥
- 读加锁和写加锁之间,互斥
- 写加锁和写加锁之间,互斥
读写锁特别适合 “频繁都,不频繁写” 的场景中
5. 可重入锁VS不可重入锁
可重入锁:就是允许同一个线程多次获取同一把锁
不可重入锁:一个线程,针对同一把锁,连续加锁两次,发生死锁
Object locker = new Object();
synchronized(locker){
synchronized(locker){
// ...
}
}
上述代码中,不会产生死锁,因为 synchronized 是可重入锁!在加锁的时候,判断当前申请锁的线程是不是已经是锁的拥有者,如果是,就直接放行了~
下面介绍一些死锁的情况:
- 1.一个线程,一把锁,可重入锁没有事,不可重入锁就会出现死锁
Object locker = new Object();
synchronized(locker){
synchronized(locker){
// ...
}
}
- 2.两个线程,两把锁,即使是可重入锁,也会死锁
Object locker1 = new Object();
Object locker2 = new Object();
// t1 线程
synchronized(locker1){
synchronized(locker2){
//...
}
}
// t2 线程
synchronized(locker2){
synchronized(locker1){
//...
}
}
上述代码,t1 线程获取 locker1 锁 ,t2 获取 locker2 锁,此时 t1要获取locker2锁时获取不到,要等t2释放以后,而t2要获取 locker1锁 之后,也必须等 t1 释放 locker1,此时就造成了死锁~~
- 3.N个线程,M把锁
这里就有个经典的哲学家,就餐问题
说到这里,我们介绍下产生死锁的四个必要条件:
- 互斥使用:一个线程拿到一把锁之后,另一个线程不能使用
- 不可抢占:一个线程拿到锁,只能自己主动释放,不能被其他线程强行占用
- 请求和保持:一个线程在拿到锁之后,不释放,同时又在申请获取其他的锁
- 循环等待:上述例子,逻辑依赖循环的
如何避免死锁~只要破除其中的任意一个条件就行,这里介绍 破坏循环等待这个条件!
- 就是针对锁进行编号,如果需要同时获取多把锁,约定加锁顺序,务必是先对小的编号进行加锁,后对大的编号加锁!
// 伪代码
Object locker1 = new Object();
Object locker2 = new Object();
// 线程A执行下面代码逻辑
synchronized(locker1){
synchronized(locker2){
//...
}
}
// 线程B执行下面代码逻辑
synchronized(locker1){
synchronized(locker2){
//...
}
}
此时就不会造成循环等待现象~
6. 公平锁VS非公平锁
假设有三个线程对一个数据进行操作,线程A先获得锁,线程B也在申请获取锁,线程C也在申请获取锁。线程B在线程C之前申请获取锁
- 公平锁:就是当线程A释放锁之后,线程B先线程C之前申请获取锁,此时就是线程B先获得,然后等线程B释放以后,线程C再获得锁
- 非公平锁:就是当线程A释放锁,线程B和线程C获取锁的先后 ,是不确定的,两个可能先于对方
7. synchronized
系统对于线程的调度是随机的,自带的synchronized这个锁是非公平的~
要想实现公平锁需要在 synchronized 的基础上,加上队列,来记录这些加锁线程的顺序
synchronized的特点:
- 既是悲观锁,也是乐观锁
- 既是轻量级锁,也是重量级锁
- 轻量级锁基于自旋锁实现,重量级锁基于挂机等待锁
- 不是读写锁
- 是可重入锁
- 是非公平锁
- 是互斥锁