死锁的三大典型情况
(1)两个进程互相都需要对对方已经加锁的对象进行加锁才能被唤醒,如:线程甲先对a加锁了,还需要对b进行加锁,线程乙先对b加锁了,还需要对a加锁,此时就陷入死锁状态,两个线程都无法对需要的对象进行加锁,出现了死锁
public satic void main(Stirng [] args){
Object a=new Object();
Object b=new Object();
Thread t1=new Thread(()->{
synchronized(a){
synchronized(b){
//线程t1先获取了a对象的锁,再获取b对象的锁
}
}
});
Thread t2=new Thread(()->{
synchronized(b){
synchronized(a){
//线程t2先获取了b对象的锁,再获取a对象的锁
}
}
});
//当线程t1或者t2在获取到第一个对象的锁后,cpu跑去执行另一个线程,
//此时就会产生死锁
}
(2)哲学家问题
一张桌子上坐着几个哲学家,每个哲学家两旁都分别放有一根筷子,哲学家平时思考问题,有时饿了就会从左右两边拿起两根筷子吃饭,当一种极限情况,所有的哲学家同时饿了开始吃饭,且都拿起了左边的筷子,此时哲学家就等到右边的筷子被释放,但是右边的筷子不可能被释放,因为每个哲学家都在等待筷子,没有人会释放
(3)不可重入锁
不可重入是指,对一个线程对一个对象加锁之后,在释放锁之前,又想对这个对象进行加锁,但是此时这个对象已经有锁了,就会陷入等待这个锁释放,但是第一个锁本身就是被线程本身加锁的,线程陷入了死锁,这个锁的特性就被称为不可重入锁,java的synchronized是可重入锁,不会出现这个问题
讲完上述的死锁的典型状况后,我们就需要来防止产生死锁,为了防止产生死锁,我们需要了解死锁产生的原因,死锁产生的原因有四个
(1)不可抢占
当一个线程获取到一个锁之后,另一个线程也想获取这个锁是不可抢占的,只能等待这个线程把锁释放才能获取到锁;
(2)请求和保持
当一个线程请求不到一个锁陷入等待后,不保持已经获得的锁,不释放
(3)互斥
一个锁每次只能被一个线程获取
(4)形成循环等待
各个线程的等待首尾相良,形成循环等待
在java中,前面三个条件是无法改变的,这是锁的特性,我们只能获取通过改变条件四来预防死锁的发生,为了防止条件4发生,我们将线程所有需要申请的锁都编个号,规定如果要申请锁只能从编号小的往编号大的申请,这样就能防止产生循环等待
为什么从编号小的往编号大的方向申请就能防止循环等待呢?
还是线程甲乙和锁ab,将锁a编号为1,锁b编号为2,此时甲乙都想申请锁a和b,他们需要现申请a,在申请b,只要甲线程申请到a,那b也只能被甲线程申请,因为乙必须先申请到a,才能申请b,而a已经被甲申请了,乙只能等待