一、什么是死锁
死锁是指在一组进程中,每个进程都在等待其他进程释放资源,而自己却不释放已经占有的资源,导致所有进程都陷入了无限等待的状态,无法继续执行。这种情况被称为死锁。
Java中,当两个或多个线程被无限期地阻塞,等待彼此释放所占用的资源时会发生死锁。死锁通常发生在多个线程同时请求多个共享资源的时候。
二、死锁的原因
发生死锁的原因主要是由于多个线程持有彼此需要的资源,又都不愿先释放手中的资源,因此无法继续执行。具体来说,发生死锁需要满足以下四个条件:
- 互斥条件:指进程对资源进行排它性控制,即在一段时间内某个资源只能被一个进程占有。
- 请求与保持条件:指进程在申请新的资源时,已经持有其他资源,但是申请的新资源被其他进程占用,此时请求资源的进程会阻塞,但是它还会继续持有已经拥有的资源。
- 不剥夺条件:指进程所占用的资源不能被其他进程抢夺,只能由该进程自己释放。
- 循环等待条件:指有两个或多个进程组成环路,每个进程都在等待下一个进程所占用的资源。
三、如何避免死锁
1. 破坏互斥条件
由于互斥条件是指对共享资源的排它性控制,但是对于允许同时访问的资源,我们可以将其从互斥资源中剥离出来。比如对于线程共同使用的读取文件指针,如果所有线程使用同一指针,会出现互斥冲突。此时,我们可以为每个线程分别创建一个读取文件指针,避免互斥。
2. 破坏请求和保持条件
为了破坏请求和保持条件,我们可以按照顺序一次性请求所有需要的资源,不再一边持有资源一边请求其他资源,避免阻塞其他线程。如果请求不到所有资源,就释放已经获得的资源,重新开始整个请求流程。
3. 破坏不剥夺条件
不剥夺条件是指占据资源的线程不能被其他线程强行剥夺资源。此时,我们可以对某些资源进行抢占,强制剥夺已经占用该资源的线程。
4. 破坏循环等待条件
为了破坏循环等待条件,我们可以使用资源有序性来防止循环等待的出现。给每个资源分配一个全局唯一的编号,每个线程按编号的顺序请求资源,释放资源则按相反的顺序进行,这样就可以消除循环等待的情况。
四、示例代码
1. 破坏互斥条件
public class NoMutexDeadlock implements Runnable { private static final Object LOCK1 = new Object(); private static final Object LOCK2 = new Object(); private boolean flag; public NoMutexDeadlock(boolean flag) { this.flag = flag; } @Override public void run() { if (flag) { synchronized (LOCK1) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (LOCK2) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK2"); } } } else { synchronized (LOCK2) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (LOCK1) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK1"); } } } } }
在这个例子中,我们定义了两个锁对象,LOCK1和LOCK2,并且两个线程在获取资源时使用不同的顺序,这样就避免了死锁的发生。
2. 破坏请求和保持条件
public class NoRequestHoldDeadlock implements Runnable { private static final Object LOCK1 = new Object(); private static final Object LOCK2 = new Object(); private boolean flag; public NoRequestHoldDeadlock(boolean flag) { this.flag = flag; } @Override public void run() { if (flag) { synchronized (LOCK1) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (LOCK2) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK2"); } } } else { synchronized (LOCK1) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK1"); synchronized (LOCK2) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK2"); } } } } }
在这个例子中,在线程获取资源时,我们一次性请求所有需要的资源,不再一边持有资源一边请求其他资源,避免阻塞其他线程。
3. 破坏不剥夺条件
public class NoDeprivationDeadlock implements Runnable { private static final Object LOCK1 = new Object(); private static final Object LOCK2 = new Object(); private boolean flag; public NoDeprivationDeadlock(boolean flag) { this.flag = flag; } @Override public void run() { if (flag) { while (true) { synchronized (LOCK1) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (LOCK2) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK2"); } } } } else { while (true) { synchronized (LOCK2) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (LOCK1) { System.out.println(Thread.currentThread().getName() + " 获取到了 LOCK1"); } } } } } } public class NoDeprivationDeadlockDemo { public static void main(String[] args) { new Thread(new NoDeprivationDeadlock(true)).start(); new Thread(new NoDeprivationDeadlock(false)).start(); } }
在这个例子中,我们使用一个while循环,强制让线程不停地执行,使得可以强制剥夺已经占用该资源的线程。
4. 破坏循环等待条件
public class NoCircularWaitDeadlock implements Runnable { private int resources; private static final Object lock = new Object(); private static int resourceCount = 0; public NoCircularWaitDeadlock(int resources) { this.resources = resources; } @Override public void run() { int waitCount = 0; while (true) { synchronized (lock) { if (resourceCount >= resources) { return; } resourceCount++; } synchronized (this) { System.out.println(Thread.currentThread().getName() + " 获取到了资源 " + resourceCount); try { if (++waitCount > 100) { System.out.println(Thread.currentThread().getName() + " 超过等待时间,退出请求"); return; } wait(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public class NoCircularWaitDeadlockDemo { public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(new NoCircularWaitDeadlock(5)).start(); } } }
在这个例子中,我们为每个资源分配一个全局唯一的编号,并且每个线程按编号的顺序请求资源,释放资源则按相反的顺序进行,这样就可以消除循环等待的情况。
五、总结
死锁问题需要我们从多个方面来解决,可以根据实际情况选择不同的方法来避免死锁的出现。