前言
本篇针对锁的使用范围进行简单介绍,让大家对锁的概念有进一步的了解,例如乐观锁与悲观锁,轻量级锁与重量级锁,自旋锁与挂起等待锁等;如有错误,请在评论区指正,让我们一起交流,共同进步!
本文开始
1. 什么是锁策略?
锁策略:解决问题的方法(合理的使用锁);
使用范围:不只是针对Java,对于别的语言涉及到锁,也同样使用;
2. 乐观锁和悲观锁
锁的实现者,预测接下来锁冲突的概率大不大,根据冲突的概率决定接下来怎么做;
这就引入了乐观锁与悲观锁;
乐观锁:预测接下来的冲突概率不大;
悲观锁:预测接下来冲突概率较大;
一般来说:
悲观锁一般做的工作更多,效率更低;乐观锁做的工作较少,效率更高一些;(这不是决定的)
3. 轻量级锁与重量级锁
轻量级锁:加锁解锁的过程更快更高效;
重量级锁:加锁解锁的过程更慢更低效;
4.自旋锁与挂起等待锁
自旋锁:自旋锁是一种典型轻量级锁;
挂起等待锁:挂起等待锁是一种典型重量级锁;
解锁自旋锁与挂起等待锁:
前提:加锁失败之后;
1.自旋锁:加锁失败后,自旋锁会立即尝试获取锁,此时会进入循环,一直循环,直到锁释放,第一时间会获取锁;(这个状态也是忙等,一直消耗cpu资源)
2.挂起等待锁:
如果锁被释放,不能第一时间拿到锁,可能需要等待很久才会拿到锁;
关于上述锁策略,Java中的 synchronized 这把锁属于那种呢?
synchronized 即使悲观锁,也是乐观锁,即使轻量级锁,也是重量级锁;轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现;
【注】synchronized 会根据当前锁的竞争激烈程度,自适应;
如果锁冲突不激烈,以轻量级锁/乐观锁的状态运行;
如果锁冲突激烈,以重量级锁/悲观锁的状态运行;
5. 互斥锁与读写锁
1.互斥锁:对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
synchronized 是互斥锁;
【注】synchronized 只有两个操作:①进入代码块,加锁 ② 出了代码块,解锁;
2.读写锁:把 “读” 和 “写” 两种加锁区分开;(适用于一写多读的情况)
【注】三种操作:①给读加锁 ②给写加锁 ③解锁
读写锁加锁规则:
①读锁与读锁之间,不会有锁竞争,不会产生阻塞等待;
②写锁与写锁之间,有锁竞争;
③读锁与写锁之间有锁竞争;
6. 可重入锁与不可重入锁
可重入锁:一个线程针对同一把锁,连续加锁两次,不出现死锁 ,就是可重入锁;
不可重入锁:一个线程针对同一把锁,连续加锁两次,出现死锁 ,就是不可重入锁;
1.什么是死锁呢?
死锁伪代码:
public static void main(String[] args) {
Object locker = new Object();
synchronized (locker) {
synchronized (locker) {
......
}
}
}
上述代码就是加锁两次的情况;
第一次加锁,第二次再尝试加锁,需要等待第一个锁释放;
第一个锁释放,需要等待第二个加锁成功;
这里逻辑上矛盾,就出现死锁了!!!
实际上synchronized 是可重入锁,不会死锁; 加锁的时候会判断,看当前尝试申请锁的线程是不是锁的拥有者;如果是,不会等待直接放行;
2.例如下面代码,就不会死锁:
两个方法都是针对this加锁,但不会死锁;
class BlockingQueue {
synchronized void put(int elem) {
this.size();
......
}
synchronized int size() {
......
}
}
3.死锁的情况:
① 一个线程,一把锁,可重入锁没啥,不可重入锁死锁;
② 两个线程两把锁, 可重入锁与不可重入锁都会死锁;
例如:
线程t1对locker1加锁,线程t2对locker2加锁,此时t1想要对locker2加锁,t2想对locker1加锁,此时逻辑矛盾,出现死锁;(先个加锁一个,还想加锁另一个,但是另一个,已经加锁了)
解决方式:(破解循环等待解决死锁,下面会介绍)
可以把线程t2的加锁顺序修改,先加locker1,再加锁locker2;
这样加锁,会出现锁竞争,但是不会死锁了;
③ n个线程,m把锁:线程数量和锁数量越多,就越容易死锁;
经典问题:哲学家就餐问题
问题描述:
五位哲学家吃饭规定:
一张桌子,上面一份饭,只有5只筷子,只有拿到两只筷子的人才能吃饭;
① 随机进行吃米饭,哲学家有两种状态,拿筷子吃饭,放下筷子;
② 规定如果哲学家想拿的筷子(左右两只筷子),被人占用了,就会等待,等待过程中,他手中已有的筷子不会放下;
发现问题:如果五位哲学家同时拿到左/右手边的筷子,就会死锁;
由以上死锁的介绍,可以发现 死锁的四个必要条件:
锁的基本特点:
① 互斥使用:一个线程拿到一把锁,另一个线程不能使用
② 不可抢占:一个线程拿到锁,只能自己主动释放,不能是被其他线程强行占用;
代码实现的特点:
③ 请求和保持:想上述哲学家问题,拿到左手边的筷子后,还想这右手边的筷子;(吃着碗里,想着锅里)
④ 循环等待:逻辑依赖于循环;(给筷子编号,规定哲学家只能在左右位置拿最小编号的筷子,此时会有哲学家没有拿到最小号筷子,就会有哲学家拿到两只筷子吃饭,吃饭完后,就会放下筷子,此时后面的人就能继续吃,这就形成了循环)
解决死锁问题:就需要破解死锁四个必要条件中的一个就可以;
最容易的就是 破解循环等待这一条件:
给每个锁 (这里指筷子) 进行编号,如果需要同时获取多把锁,约定加锁顺序,务必先对小的编号加锁,再对大的编号加锁;
解决哲学家就餐死锁问题过程(可根据下图理解):
每个人只能先拿到小编号的筷子,假设从2号哲学家开始,最后1号哲学家拿不到编号小的筷子(1号筷子被2号拿了),此时5号哲学家就能拿5号筷子,首先完成就餐,接着放下筷子,4号哲学家开始,依次类推,3,2,1再完成就餐;
7. 公平锁和非公平锁
公平锁:遵守先来后到规则;
先来的先加锁,后来的后加锁,此时就是公平锁(A比B先来,A就先加锁,A释放,B再加锁);
非公平锁:不遵守先来后到;(B比A先来,但是谁都可能先加锁)
【注】这里等概率竞争,认为是不公平的;
系统对于线程的调度是无序的,系统自带的 synchronized锁是非公平的;
synchronized特点:
① 是乐观锁,也是悲观锁;
② 是轻量级锁,也是重量级锁;
③ 轻量级锁基于自旋锁实现,重量级锁基于挂起等待实现;
④ synchronized不是可读写锁;
⑤ 是可重入锁;
⑥ 是非公平锁;
总结
✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!