在没有讲解死锁的概念之前,我们先从一个生活中的案例来体会一下:
假设有甲乙两个人在就餐,每个人就餐必须同时拥有一把餐刀和一把叉子。但目前餐具不足,统共只有一把餐刀和一把叉子。现在,甲拿到了一把餐刀,乙拿到了一把叉子,他们都无法吃到饭。于是,就有了下面的对话:
甲:“乙你先给我叉子,我再给你餐刀!”
乙:“甲你先给我餐刀,我才给你叉子!”……
如果甲乙双方都不让步,那么局面就会一直僵持下去,他们只能一直等下去,只到等死——这就是“死锁”在生活中的影子,如下图所示:
如果将上面案例的人数扩展到5人,就是著名计算机科学家迪科斯彻(Dijkstra) 1965年提出的经典的同步问题—— “哲学家进餐(Dining philosophers problem)” 。
在操作系统中,计算资源不足是常态。通常我们使用共享的方法来解决资源不足的问题。
例如,操作系统中有很多执行中的程序——进程(或线程),都想使用打印机,在一般情况下,我们不会为每个执行中的程序配备一台打印机,而是让它们共享一台打印机。有些设备(如打印机)是独占设备,即一个进程(或线程)在使用该设备时,其他进程(或线程)是不能使用的。
为了达到这个目的,操作系统使用了“锁”的概念,来保证对某个设备的独占访问。
独占设备也是可以实现资源共享的,但它们的共享是通过 “我完全不用时,你再用” 来实现的。
进程或线程在执行过程中,通常需要不止一种计算资源,一旦有多个进程或线程已经分配部分独占设备,同时又想申请其他进程或线程占据的独占设备,那么就有可能发生死锁。 如果有一组进程(或线程),其中的每一个都无限等待被该组进程(或线程)中另一进程(或线程)所占有的资源,就会产生无限期地僵持下去的局面,这种现象称为死锁。
具体到Java中的多线程编程,最常见的死锁形式是当线程1已经占据资源R1,并持有资源R1上的锁,而且正在等待资源R2的开锁;而线程2已经占据资源R2,并持有资源R2上的锁,却正在等待资源R1的开锁。如果两个线程不释放自己占据的资源锁,而还申请对方资源的上的锁,申请不到时就等待,那么它们只能永远等待下去。
要预防死锁有多种方法,其中一种就是利用有序资源分配策略,把系统的所有资源排列成一个顺序。
例如,系统若共有n个线程,共有m个资源,用Ri(1≤i≤m)表示第i个资源,于是这m个资源是:如果规定线程i在占用资源Ri时候不得再申请Rj(i>j),即在申请多项资源时,这种策略要求线程申请资源必须按以编号上升的次序依次申请——这样做,在本质上破坏了死锁的必要条件——循环等待。
回到刚才的例子,如果按照有序资源分配的策略,线程1必须先得到资源R1,然后再申请资源R2。而线程2也必须先得到资源R1,才能申请资源R2,即使它需要先使用资源R2,在线程2没有得到R1之前,它被禁止申请资源R2。虽然一开始线程2可能因为等待R1且不占据R2而浪费一些时间,但是这种等待和“奉献精神”换来了避免死锁的发生。
**在真实程序中,死锁是较难发现的。**在下面的例子模拟了死锁的发生:
package com.xy.test3;
public class DeadLockDemo {
static String knife = "餐刀", fork = "叉子";
static class A extends Thread {
public void run() {
synchronized(knife) {
System.out.println("甲拿起了" + knife + ", 等待" + fork + "...");
try {
Thread.sleep(100);
}
catch(InterruptedException e) {
synchronized(fork) { // 由于死锁,这段代码永远不会执行
System.out.println("甲双拿起了" + fork);
}
}
}
}
}
static class B extends Thread {
public void run() {
synchronized(fork) {
System.out.println("乙拿起了" + fork + ", 等待" + knife + "...");
try {
Thread.sleep(100);
}
catch(InterruptedException e) {
synchronized(fork) { // 由于死锁,这段代码永远不会执行
System.out.println("乙双拿起了" + knife);
}
}
}
}
}
static class Demo extends Thread {
public Demo() {
this.setDaemon(true); // 设置守护线程
}
public void run() {
while(true) {
try {
Thread.sleep(1000);
}
catch(InterruptedException e) {
System.out.println("守护线程:程序运行中...");
}
}
}
}
public static void main(String[] args) {
new A().start();
new B().start();
new Demo().start();
}
}
【结果】