🔎乐观锁与悲观锁
锁🔒冲突
多个线程针对同一个对象进行加锁🔒,可能产生阻塞等待
乐观锁🔒
预测接下来锁🔒冲突的概率不大
乐观锁总是假设最好的情况,认为一般情况下不会产生并发冲突,所以只在数据进行提交修改的时候去对数据进行冲突检测
悲观锁🔒
预测接下来锁🔒冲突的概率较大
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据时都会上锁
🔎轻量级锁与重量级锁
轻量级锁🔒
加锁解锁的过程更快更高效
重量级锁🔒
加锁解锁的过程更慢更低效
注意
一个乐观锁🔒很可能也是一个轻量级锁🔒(不绝对)
一个悲观锁🔒很可能也是一个重量级锁🔒(不绝对)
🔎自旋锁与挂起等待锁
自旋锁🔒是轻量级锁的一种典型实现
挂起等待锁🔒是重量级锁的一种典型实现
举个栗子🥝
有一个老哥去追他喜欢的女神
问女神,你能做我女朋友吗
女神说:对不起,你是个好人
(老哥对女神加锁失败)
自旋锁🔒
这时候老哥选择继续每天向女神问候早安,午安,晚安
一旦哪一天,女神和她男朋友分手了
那老哥的机会就来了(有很大的机会加锁🔒成功)
自旋锁的意思就是一旦锁🔒被释放,就能第一时间拿到锁,速度更快
缺点是每天都得围着女神转(等待加锁),不能去做其他的事情(忙等,消耗cpu)
注意
自旋锁通常是纯用户态的,不需要经过内核态,时间相对较长
挂起等待锁🔒
这时候老哥选择果断放弃,潜心敲代码
等到哪一天女神分手(这中间不知道分手了多少个),最后找到了老哥求安慰,老哥再次尝试加锁
挂起等待锁的意思是锁🔒被释放,不能第一时间拿到锁,可能要很久才能拿到锁
优点是时间是空闲出来的(不用忙等,消耗cpu)
注意
挂起等待锁通过内核的机制(内核态)实现挂起等待,时间相对较长
🔎互斥锁与读写锁
互斥锁🔒
synchronized 只有两个操作
(1)进代码块,加锁
(2)出代码块,解锁
synchronized 是互斥锁🔒
加锁,就只是单纯的加锁,没由更进一步的划分了
读写锁🔒
(1)给读加锁
(2)给写加锁
(3)解锁
读写锁中约定
(1)读锁与读锁之间,不会产生锁竞争(不会影响程序速度)
(2)写锁与写锁之间,会产生锁竞争(会降低程序速度,但保证数据准确性)
(3)读锁与写锁之间,会产生锁竞争(会降低程序速度,但保证数据准确性)
注意
读写锁更适合于频繁读不频繁写的情况
Java标准库中提供了 ReentrantReadWriteLock 类,实现了读写锁
(1) ReentrantReadWriteLock.readLock 表示读一个锁
(2) ReentrantReadWriteLock.writeLock 表示写一个锁
🔎可重入锁与不可重入锁
可重入锁🔒
一个线程针对一把锁,连续加锁两次,没出现死锁,就是可重入锁🔒
不可重入锁🔒
一个线程针对一把锁,连续加锁两次,出现死锁,就是不可重入锁🔒
死锁🔒
举个栗子🥝
车钥匙忘在家里
家钥匙忘在车里
如果想拿到车钥匙就得先进入家里
如果想进入家里就得先拿到车钥匙
关于死锁🔒
case1🥝 一个线程一把锁
private static Object locker = new Object(); public static void main(String[] args) { synchronized (locker) { synchronized (locker) { } } }
形如这样的代码,就是一个线程连续对一把锁加锁两次
加锁的时候判定一下看当前申请锁的线程是不是锁的拥有者(第一次加锁的线程)
如果是
第二次加锁成功(可重入锁🔒)
如果不是
第二次加锁需要等待第一次加锁的释放,第一次加锁的释放需要等待第二次加锁的成功
这样就形成了死锁(不可重入锁🔒)
case2🥝 两个线程两把锁
public class Test { private static Object locker1 = new Object(); private static Object locker2 = new Object(); static class Conuter { public static int count = 0; public void add1() { synchronized (locker1) { synchronized (locker2) { count++; } } } public void add2() { synchronized (locker2) { synchronized (locker1) { count++; } } } } }
形如这样的代码,就是两个线程对两把锁进行加锁
线程1执行add1()方法,对 locker1进行加锁
此时线程2执行add1()方法,发现add1()方法被线程1加锁,只能等待
线程2执行add2()方法,对 locker2进行加锁
此时线程1执行add2()方法,发现add2()方法被线程2加锁,只能等待
由于add1()方法里面还需一个 locker2加锁,故线程1不能执行add1()方法
由于add2()方法里面还需一个 locker1加锁,故线程2不能执行add2()方法
但是线程2对locker2加锁需要等待线程1将 locker1释放
但是线程1对locker1加锁需要等待线程2将 locker2释放
于是就形成了死锁
case3🥝 M个线程N把锁(哲学家就餐问题)
当线程和锁的数量更多时,就更容易发生死锁问题了
举个栗子🥝
假设有5位哲学家
(1)每个人随机的进行吃面条(拿起筷子)和思考人生(放下筷子)
(2)这5位哲学家比较固执(如果他想拿筷子,却被别人占用了,就会进行等待,等待的过程中也不会放下手中的筷子)
注意
哲学家 ≈ 线程
筷子 ≈ 锁
假设5位哲学家同时拿起各自左手边的筷子,那么此时就会发生死锁🔒的情况(每个人都吃不到面条)
解决方案🌸
为筷子(锁)加编号
如果需要同时获取多把锁,约定加锁顺序(从编号小的开始加锁)
为了避免死锁的情况,给筷子(锁)加了编号
规定每个人先从编号小的筷子拿起,再拿编号大的筷子
于是
2号哲学家拿了筷子1
3号哲学家拿了筷子2
4号哲学家拿了筷子3
5号哲学家拿了筷子4
由于此时1号哲学家没能拿到编号较小的筷子1,所以他也不能拿到筷子5
这时5号哲学家拿起了筷子5,吃完后放下筷子(思考人生)
4号哲学家等5号哲学家放下筷子4,成功拿起筷子4,再结合手中的筷子3,顺利吃上面条
3号哲学家等4号哲学家放下筷子3,成功拿起筷子3,再结合手中的筷子2,顺利吃上面条
…
哲学家就餐问题就被完美解决了
对死锁🔒的总结
死锁的形成有四个必要条件
(1)互斥使用
一个线程拿到一把锁之后,另一个线程不能使用
(2)不可抢占
一个线程拿到锁之后,只能自己释放,其他线程不能强行占有(挖墙脚行为不道德)
(3)请求和保持
一个线程拿到一把锁之后(保持),继续申请其他的锁(请求)
一位哲学家拿到一根筷子后,不放下,继续申请其他的筷子
(4)循环等待
不戴口罩不能进药店,进药店才能买口罩戴
🔎公平锁与非公平锁
公平锁🔒
遵守先来后到
非公平锁🔒
不遵守先来后到
举个栗子🥝
一群老哥追一个女神
老哥A:我认识了一年
老哥B:我认识了5个月
老哥C:我认识了1个月
公平锁🔒
老哥A先来的,让老哥A先去追,
非公平锁🔒
凭啥你先来的你就能先追
大家各凭本事,谁追到算谁的
此处虽然是等概率竞争,但并未遵循先来后到原则,所以等概率也就成了非公平
🔎synchronized的特点
(1)既是乐观锁,也是悲观锁
(2)既是轻量级锁,也是重量级锁
(3)轻量级锁基于自旋锁实现,重量级锁基于挂起等待锁实现
(4)是互斥锁,不是读写锁
(5)是可重入锁
(6)是非公平锁
🔎结尾
创作不易,如果对您有帮助,希望您能点个免费的赞👍
大家有什么不太理解的,可以私信或者评论区留言,一起加油