几张图解释清楚死锁+wait和notify的搭配使用
死锁🚕
-
synchronized在上一篇博客中进行介绍时,提到,一旦一个对象被上锁,那别的线程就无法再给这个对象上锁,但如果这个线程是自身呢?也就是说一个线程尝试给同一个对象上锁两次会出现什么结果?比如见下面的代码;
class Counter1{ private int count=0; public synchronized void increase1(){ this.count++; } public synchronized void increase2(){ this.count++; } public int getCount(){ return this.count; } } public class Demo4 { public static void main(String[] args) { Counter1 counter=new Counter1(); counter.increase1(); counter.increase2(); System.out.println(counter.getCount()); } } //打印2
上述代码中,main线程中先后执行了increase1和increase2,这两个操作都会先给Counter1对象进行上锁,按照synchronized的理解,主线程的任务是执行完两次increase才算完成任务,所以说明了一个线程对同一个对象加锁两次,但是代码却正常运行出了结果.
这正是由于synchronized是一个可重入锁
可重入锁:同一个线程使用synchronized给同一个对象进行上锁,没有任何问题,此时可重入锁内部会记录当前的锁被哪个线程所持有,并且记录当前线程已经给这个锁上几次了,此时连续上锁的操作就不会导致一个死锁.此时会记录该线程已经为这个锁对象上了多少次锁.
-
一把锁,一个线程的情况:
如果synchronized不是可重入锁,一个线程尝试先后两次给同一个对象进行上锁,由于synchronized将赋予锁不可被抢占,并且呢线程只会在synchronized锁上的代码块都执行完了才会把锁对象进行释放,如果出现下面的代码:
synchronized(this){ synchronized(this){ ... } }
不给上两次锁我就没办法把任务执行完,任务直行不完,我又办法释放锁,陷入僵局…即死锁.
-
两个线程两把锁
两个线程先各自锁上一个对象,但是每个线程的任务后续必须还要获取到对方已经获取到的锁,才能把整个任务给执行完,但是每个线程任务执行不完,又都没办法把各自锁持有的锁给释放,并且锁天生有不可被抢占.所以陷入僵局…即死锁
-
N个线程M把锁
形成环路等待(还是由于不可抢占导致),就会陷入僵局…即死锁.
比如有一个故事:哲学家吃面条
故事:每个哲学家都非常的固执,都是要先拿起左手边的筷子,再拿起右手边的筷子(左手边没筷子了,就拿右手边的筷子),再吃面条,再放下筷子.只有没有拿起一双筷子,哲学家就会一直等着,直到能拿起一双筷子了,就会吃面条.
但是因为都先拿起了左手边的筷子(倒霉鬼先拿起右手边的筷子),等到要拿右手边的时候,筷子总被其他人占着,但是每个哲学家只有拿不到一双筷子就会一直等,那此时就会陷入僵局,谁也别吃面条了.这就是形成了死锁.
-
总结死锁出现的必要条件:
- 锁不可被抢占
- 锁是互斥使用的(一个时刻,锁最多只能被一个线程所持有)
- 请求和等待,锁被别的线程持有了,那本线程就会一直等着锁对象被释放,然后去尝试上锁
- 环路等待,就如上述的哲学及吃面条一样.
-
如何解决死锁问题
只要约定好加锁的规则,就可以避免死锁
比如上述哲学家吃面条的故事,给筷子事先编号:
并且每个哲学家都会先拿编号小的筷子,有编号更小的筷子被别人拿走了,我就先等着,等到小编号的筷子被释放了,我才会去拿小编号筷子.
第二步:拿身边较大编号的筷子:
假设老铁1先拿起一双筷子,他吃完就可以放下1,5两根筷子,那此时老铁2就可以拿到1号筷子,5号老铁就可以拿到5号筷子,那老铁5也可以吃面条了,吃完面条放下筷子,4号老铁也可以吃面条,然后是3号老铁,最后是2号老铁,最终大家都吃到了面条,场面一片和谐…
wait()和notify()的使用🌊
等待和通知
前面博客中有使用到join,注意join()表明的是谁等谁,如果在main线程中调用t.join(),意思是mian线程再等t线程执行任务完成,mian线程再从这一行接着往下执行.
此处的wait和notify一般搭配synchronized进行使用.目的是让随机调度线程变得有固定的调度顺序.
wait和notify都是object的方法.谁调用wait方法,谁(某个线程)就会陷入阻塞(waiting状态),当有另一个线程使用notify通知,进行唤醒之后,方可从刚才阻塞的代码继续往下执行.
-
wait调用后会干三件事:
- 释放所对象
- 等待其他线程来唤醒
- 重新获取到锁,并接着阻塞的代码继续往下执行
-
wait和notify必须是针对同一个锁对象来进行操作
-
wait和notify都必须先把所对象给锁上了才能进一步使用
-
具体:
public class Demo5 { private static Object locker=new Object(); public static void main(String[] args) { Thread t1=new Thread(()->{ synchronized (locker){ try { System.out.println("wait之前"); locker.wait(); System.out.println("wait之后"); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); Thread t2=new Thread(()->{ synchronized (locker){ System.out.println("notify之前"); locker.notify(); System.out.println("notify之后"); } }); t2.start(); } } //打印: wait之前 notify之前 notify之后 wait之后
-
notify即使去尝试唤醒一个没进行wait的线程时,也不要紧.
-
如果针对同一个锁先后有10个线程对其进行了wait,之后有一个线程对locker进行为唤醒,此时是随机唤醒10个waiting中的一个线程.但如果使用**notifyAll()**就会唤醒所有waiting中的线程哦(都是针对locker正在waiting的线程才行哦)
conclusion⛵️
- 什么是可重入锁
- 什么是死锁
- 死锁有哪些情形
- 死锁怎么取解决
- wait和notify怎么去使用
-