🌈个人主页: Aileen_0v0
🔥热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法
💫个人格言:“没有罗马,那就自己创造罗马~”
文章目录
- 温故而知新
- 线程安全问题
- 多线程中有的线程未加锁
- 一个线程有多把锁
- 加了多层锁的代码,执行到最后一个 } 才真正解锁
- 死锁三大场景
- 场景1: 非可重入锁 ——(多套几个锁即可解决死锁)
- 场景2: 两个线程两把锁【你在等我,我也在等你,不主动就永远没有故事直到死亡~】—— (通过jconsole调用栈+状态定位解决)
- 场景3:N个线程,M把锁
- 死锁
- ⭐️⭐️⭐️⭐️⭐️死锁的四个必要条件【缺一不可,任何一个死锁的场景都必须同时具备这四个条件,少一个都不会发生死锁。】
- 从第三点出发解决死锁问题
- 从第四点出发解决死锁问题(简单且高效的方法)
- 多个线程获取多把锁避免死锁的解决办法
温故而知新
线程安全问题
对比上面的两段代码,我们可以看到,当我们
i
的的值 循环次数较少时,发生线程安全的问题明显就减小了,但它任然存在线程安全问题。
上面代码中当i很小时,t1就开始计算了,可能会出现:t2还没启动,t1就运行完了,此时这两个线程就相当于是串行执行。
通过对比上面两段多线程代码的运行结果:我们可以得出多线程代码运行具有随机性。
package thread;
class Counter2{
private int count = 0 ;
void add(){
count++;
}
int get(){
return count;
}
}
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
Counter2 counter2 = new Counter2();
Thread t1 = new Thread(() -> {
for(int i = 0 ; i < 5000 ; i++){
synchronized (counter2){
counter2.add();
}
}
});
Thread t2 = new Thread(() -> {
for(int i = 0 ; i < 5000 ; i++){
synchronized (counter2){
counter2.add();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + counter2.get());
}
}
如果一个线程加了锁另一个线程不加锁,那线程还安全吗?
多线程中有的线程未加锁
多线程中如果有的线程未加锁,会发生线程不安全的情况,上面代码中
t2
未加锁,即使t1
加锁了,由于t2
没有任何阻塞,没有互斥,任然会使t1++
到一半的时候,被t2
进来把结果覆盖掉。
故事时间:上面的就好比两个男的追一个女的,但是这个女的和其它男的谈了,但是这两个男的依旧是穷追不舍,其中一个呢就比较老实一点,默默观望;另一个就是霸王硬上弓,恨不得打一架
一个线程有多把锁
假设t2先启动(t1先不考虑),t2线程第一次加锁肯定能成功,但当t2尝试第二次加锁时,此时counter2变量,属于已被锁定的状态,根据之前的知识,当我们针对一个已被锁定的线程加锁就会出现阻塞等待,并且会一直阻塞到对象被解锁时。
想要获取第二把锁,就需要先给第一把锁解锁,但是想要给第一把锁解锁就需要执行完第一层大括号,但执行完第一层大括号又需要先获取第二层锁,这两层加锁解锁操作相互矛盾,这种情况就叫做“死锁”。
但是根据上面的运行结果,我们可以看到,这个结果是正确的,也就是说上述死锁过程对于“synchronized”并不适用,但是对于C++/python 就会出现死锁的现象。
synchronized 之所以没出现上面死锁现象的原因,是因为自己在内部(JVM)做了特殊处理——每个锁对象里会记录当前哪个线程持有了这个锁,如果下次再加锁,发现他是针对同一个线程加锁就不会去调用操纵系统的阻塞加锁了,这就可以避免出现死锁的现象。(tip:两把锁可以开同一个开一个门,我只要其中一把即可,另一把可忽略使用) |
加了多层锁的代码,执行到最后一个 } 才真正解锁
但是如果有N层锁,如何判定这个 } 是最外层的 } ,JVM如何识别? |
- 解决方案⚠️:“引用计数器”:在锁对象里面维护一个计数器(int n):
- 每次遇到 { ,n 就 ++ (只有第一次才真正加锁)
- 每次遇到 } , n 就-- (当 n 减到0了,才真正解锁)
- 也就是说里层加锁是虚而不实的,一般来说,只需要在外层加锁即可。【之所以要在里层加锁,是为了防止出现死锁,这种机制叫 “可重入锁”】
死锁三大场景
场景1: 非可重入锁 ——(多套几个锁即可解决死锁)
场景2: 两个线程两把锁【你在等我,我也在等你,不主动就永远没有故事直到死亡~】—— (通过jconsole调用栈+状态定位解决)
- 现有线程1和线程2,以及有锁A和B,现在线程1和线程2都需要获取锁A和锁B,但是拿到锁A之后,不释放A,继续获取锁B
故事时间:疫情期间,有一个地方的健康码崩了,程序员赶紧回公司去修复Bug,但他在楼下被保安拦住要求出示健康码才能上楼,但是程序员如果没有上楼修复好Bug就无法出示健康码,如果两个人你不让我我不让你,就会一直僵持,这就是死锁。
- 代码示例:
package thread;
public class Demo22 {
public static void main(String[] args) throws InterruptedException {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized(locker1){
//为了更好的控制线程的执行顺序,引入sleep,否则死锁可能会重现不出来
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(locker2){
System.out.println("t1 获取两把锁");
}
}
});
Thread t2 = new Thread(() -> {
synchronized(locker2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(locker1){
System.out.println("t2 线程拿到两把锁");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
-
上面代码中,先分别让t1和t2拿到一把锁,然后再尝试去获取对方的锁。
-
根据上面的运行结果,我们可以看到进程并未退出,也未打印线程中的内容,这就是死锁现象。
-
上图是在jconsole找到阻塞的具体代码位置。并且在Java的锁对象中会记录当前是哪个线程持有这把锁。
-
如果只有一个线程,就不会触发锁竞争,也就不会发生阻塞现象。
场景3:N个线程,M把锁
-
随着线程数目/锁个数的增加,此时,情况更加复杂,就更容易出现死锁。
- 案例:哲学家问题
- 案例:哲学家问题
-
每个滑稽都坐在每个筷子之间,每个滑稽都要做两件事情:
- 1.思考人生(放下筷子)
- 2.吃面条(需要拿起左右两根筷子)
-
每个哲学家啥时候吃面条,啥时候思考人生都是不确定的(抢占式执行)
-
上面的模型,大部分可正常工作,但是如果出现极端情况就会出现问题:
-
同一时刻,所有滑稽老铁,都拿起左边的筷子,此时,所有滑稽老铁都无法拿起右边的筷子,并且每个滑稽都是比较固执的人,每个哲学家只要吃不到面条,就不会放下手中的筷子。
-
上面就是典型的死锁状态,更多的哲学家,更多的筷子,情况也类似
死锁
- 死锁:死锁是非常严重的问题,他会使线程被卡主,无法继续工作,死锁这种Bug的出现具有概率性,测试的时候啥事没有,但是一发布就出问题,即使一发布了没问题,等到大家睡着了说不定又出现Bug。
⭐️⭐️⭐️⭐️⭐️死锁的四个必要条件【缺一不可,任何一个死锁的场景都必须同时具备这四个条件,少一个都不会发生死锁。】
- 1.锁具有互斥性 (基本特点:一个线程拿到锁以后,其他线程就阻塞等待)
- 2.锁不可抢占(不可被剥夺)一个线程拿到锁以后,除非它自己主动选择释放锁,否则别人抢不走(锁的基本特点)
- 3.请求和保持,一个线程拿到一把锁以后,不释放这个锁的情况下,再尝试去获取其它锁。
嵌套锁
【代码结构】 - 4.循环等待。(多个线程获取多个锁的过程中,出现了循环等待,A等待B,B又等待A)
你等我我等你,谁都不主动的死等
【代码结构】
⚠️注意:如果是自己实现的锁,就可以实现打破互斥,打破不可剥夺这两个条件,但是对于synchronized
这样的锁就不行。
从第三点出发解决死锁问题
- 我们将刚刚的死锁代码进行修改,根据第三点既然嵌套会发生死锁,那我们可以将这两个锁分开,不让它们嵌套,就可以解决死锁问题
从第四点出发解决死锁问题(简单且高效的方法)
- 第四点就是你等我,我等你,互不相让造成的死锁,要解决这个问题,就需要破除循环等待,(双向奔赴)约定好加锁顺序,让所有线程按照固定顺序来获取锁,这样即使出现第三点的嵌套也不会产生死锁现象。eg:约定必需先获取 locker1 后获取 locker2 。
- 上面的代码中约定了完成 t1 执行的逻辑以后,释放完 locker1 之后,才轮到 t2 执行
多个线程获取多把锁避免死锁的解决办法
- ⭐️⭐️⭐️⭐️⭐️当代码中,需要用到多个线程获取多把锁,一定要约定好加锁顺序,可以有效避免死锁。如下所示:
- 上图中滑稽老铁就餐问题,我们约定每个滑稽必需从编号小的筷子开始获取,然后才能获取编号大的;从2号老铁开始拿筷子,每人左手拿一只,但是一号老铁非要拿1号的筷子,这就会导致,1和2老铁之间发生线程阻塞;为了解决这个问题,我们可以做以下操作:
- 先让5号老铁用4号和5号筷子,等他吃完释放筷子以后,4号老铁就可以拿起4号和3号筷子,4号吃完释放以后,3号老铁就可以拿起3号和2号筷子吃面,吃完释放,2号就可以拿起2号和1号筷子,吃完释放后,1号老铁就能拿起1号和5号筷子去吃面条了,这就能保证每个老铁都能根据自己的需求拿到对应号码的筷子去吃面条,避免阻塞等待。