五、活跃性问题
活跃性问题包括死锁、活锁、饥饿,最著名的就是死锁问题了。
5.1、死锁
5.1.1、什么是死锁
系统中的每一个线程都持有着其他线程需要的资源,同时又想获取其他线程已经拥有的资源,并且每个线程在获取全部想要的资源之前不会释放已经拥有的资源,那么这种情况就称为死锁。
例如:当线程一持有A锁,想要获取B锁的同时,线程二持有者B锁,并且想要获取A锁。那么线程一和线程二就产生了死锁,两个线程会永远的阻塞下去。
5.1.2、死锁代码
public class DeadLockDemo {
public static void main(String[] args) throws Exception{
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("t1获取lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("t1获取lock2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("t2获取lock2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("t2获取lock1");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("执行完毕");
}
}
执行结果:
t1获取lock1
t2获取lock2
5.1.3、如何辨别死锁代码
(1)辨明代码中所有的同步代码共有几个锁对象,如果只有一个,是不会产生死锁的,如果有多个则需要小心;
(2)在同步代码块中,是否有调用其他方法的情况,如果有就有很大的风险。
(3)查看这些同步代码块调用其他同步方法的代码的获取锁的顺序,如果多个这种调用的情况顺序是不一样的,那么就有可能产生死锁。
5.1.4、死锁的必要条件
(1)互斥条件:一个资源一次只能有一个线程可以使用,如果有另一个线程申请该资源,那么这个线程进入阻塞状态知道资源被释放;(synchronized独占锁)
(2)请求与保持条件:一个线程因请求了被其他线程占用的资源而进入阻塞状态时,这个线程对自己持有的资源不会释放;
(3)不剥夺条件:一个线程获取了某个资源后,只能进程自己资源释放资源,不能强行剥夺;
(4)循环等待条件:有一组等待线程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。
这四个条件是形成死锁的必要条件,非充分条件,即同时满足这四个条件也不一定会产生死锁,但是产生了死锁,肯定是满足了这四个条件。只有上诉四个条件之一不满足,肯定不会产生死锁。
5.1.5、如何避免死锁
(1)开放调用
不要在持有锁的情况下调用其他方法,所以要尽量缩小锁的范围,只对共享可变变量的操作上锁即可。
这种方法是避免了循环等待条件的形成。
(2)固定获取资源的顺序
对于线程获取资源的顺序进行限制,可以对资源进行大小排序,获取资源时要从最大的资源开始获取。
这种方法也是避免了循环等待条件的形成。
(3)使用超时锁
超时锁可以在锁使用一段时间后,自动释放锁,详见?显示锁
这种方法是打破了不剥夺条件。
5.2、饥饿
由于进程的优先级过低而一直获取不到cpu的执行权。这种问题称为饥饿。
所以我们尽量不要设置进程的优先级,都是用默认优先级即可。
5.3、活锁
当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行,就发生了活锁。
避免活锁的方法是在重试机制中加入随机性。