参考答案
并发程序一旦死锁,通常只能重启应用。解决死锁问题,最好的办法是:避免死锁。
一、死锁发生的条件
- 互斥,共享资源只能被一个线程占用;
- 占有且等待,线程 t1 已经取得共享资源 s1,尝试获取共享资源 s2 的时候,不释放共享资源 s1;
- 不可抢占,其他线程不能强行抢占线程 t1 占有的资源 s1;
- 循环等待,线程 t1 等待线程 t2 占有的资源,线程 t2 等待线程 t1 占有的资源。
二、避免死锁的方法
- 对于以上 4 个条件,只要破坏其中一个条件,就可以避免死锁的发生。
- 对于第一个条件 “互斥” 是不能破坏的,因为加锁就是为了保证互斥。
- 其他三个条件,可以做以下尝试:
- 一次性申请所有的资源,破坏 “占有且等待” 条件;
- 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 “不可抢占” 条件;
- 按序申请资源,破坏 “循环等待” 条件。
三、编程中的最佳实践
- 使用 Lock 的 tryLock(long timeout, TimeUnit unit)的方法,设置超时时间,超时可以退出防止死锁;
- 尽量使用并发工具类代替加锁;
- 尽量降低锁的使用粒度;
- 尽量减少同步的代码块。
四、实例
1. 使用管理类一次性申请所有的资源,破坏 “占有且等待” 条件实例:
import java.util.HashSet; import java.util.Set; /** * 测试 一次性申请所有的资源,破坏 "占有且等待" 条件示例 * @author ConstXiong * @date 2019-09-24 14:04:12 */ public class TestBreakLockAndWait { //单例的资源管理类 private final static Manger manager = new Manger(); //资源1 private static Object res1 = new Object(); //资源2 private static Object res2 = new Object(); public static void main(String[] args) { new Thread(() -> { boolean applySuccess = false; while (!applySuccess) { //向管理类,申请res1和res2,申请失败,重试 applySuccess = manager.applyResources(res1, res2); if (applySuccess) { try { System.out.println("线程:" + Thread.currentThread().getName() + " 申请 res1、res2 资源成功"); synchronized (res1) { System.out.println("线程:" + Thread.currentThread().getName() + " 获取到 res1 资源的锁"); //休眠 1秒 try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } synchronized (res2) { System.out.println("线程:" + Thread.currentThread().getName() + " 获取到 res2 资源的锁"); } } } finally { manager.returnResources(res1, res2);//归还资源 } } else { System.out.println("线程:" + Thread.currentThread().getName() + " 申请 res1、res2 资源失败"); //申请失败休眠 200 毫秒后重试 try { Thread.sleep(200); } catch (Exception e) { e.printStackTrace(); } } } }).start(); new Thread(() -> { boolean applySuccess = false; while (!applySuccess) { //向管理类,申请res1和res2,申请失败,重试 applySuccess = manager.applyResources(res1, res2); if (applySuccess) { try { System.out.println("线程:" + Thread.currentThread().getName() + " 申请 res1、res2 资源成功"); synchronized (res2) { System.out.println("线程:" + Thread.currentThread().getName() + " 获取到 res1 资源的锁"); //休眠 1秒 try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } synchronized (res1) { System.out.println("线程:" + Thread.currentThread().getName() + " 获取到 res2 资源的锁"); } } } finally { manager.returnResources(res1, res2);//归还资源 } } else { System.out.println("线程:" + Thread.currentThread().getName() + " 申请 res1、res2 资源失败"); //申请失败休眠 200 毫秒后重试 try { Thread.sleep(200); } catch (Exception e) { e.printStackTrace(); } } } }).start(); } } /** * 资源申请、归还管理类 * @author ConstXiong * @date 2019-09-24 14:10:57 */ class Manger { //资源存放集合 private Set<Object> resources = new HashSet<Object>(); /** * 申请资源 * @param res1 * @param res2 * @return */ synchronized boolean applyResources(Object res1, Object res2) { if (resources.contains(res1) || resources.contains(res1)) { return false; } else { resources.add(res1); resources.add(res2); return true; } } /** * 归还资源 * @param res1 * @param res2 */ synchronized void returnResources(Object res1, Object res2) { resources.remove(res1); resources.remove(res2); } }
结果:
线程-1 在线程-0 释放完资源后,才能成功申请 res1 和 res2 的锁。
线程:Thread-0 申请 res1、res2 资源成功 线程:Thread-0 获取到 res1 资源的锁 线程:Thread-1 申请 res1、res2 资源失败 线程:Thread-1 申请 res1、res2 资源失败 线程:Thread-1 申请 res1、res2 资源失败 线程:Thread-1 申请 res1、res2 资源失败 线程:Thread-1 申请 res1、res2 资源失败 线程:Thread-0 获取到 res2 资源的锁 线程:Thread-1 申请 res1、res2 资源失败 线程:Thread-1 申请 res1、res2 资源成功 线程:Thread-1 获取到 res1 资源的锁 线程:Thread-1 获取到 res2 资源的锁
2. 使用 Lock 的 tryLock() 方法,获取锁失败释放所有资源,破坏 “不可抢占” 条件实例
import java.util.Random; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 测试 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 "不可抢占" 条件 * @author ConstXiong * @date 2019-09-24 14:50:51 */ public class TestBreakLockOccupation { private static Random r = new Random(); private static Lock lock1 = new ReentrantLock(); private static Lock lock2 = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { //标识任务是否完成 boolean taskComplete = false; while (!taskComplete) { lock1.lock(); System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock1 成功"); try { //随机休眠,帮助造成死锁环境 try { Thread.sleep(r.nextInt(30)); } catch (Exception e) { e.printStackTrace(); } //线程 0 尝试获取 lock2 if (lock2.tryLock()) { System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock2 成功"); try { taskComplete = true; } finally { lock2.unlock(); } } else { System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock2 失败"); } } finally { lock1.unlock(); } //随机休眠,避免出现活锁 try { Thread.sleep(r.nextInt(10)); } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(() -> { //标识任务是否完成 boolean taskComplete = false; while (!taskComplete) { lock2.lock(); System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock2 成功"); try { //随机休眠,帮助造成死锁环境 try { Thread.sleep(r.nextInt(30)); } catch (Exception e) { e.printStackTrace(); } //线程2 尝试获取锁 lock1 if (lock1.tryLock()) { System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock1 成功"); try { taskComplete = true; } finally { lock1.unlock(); } } else { System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock1 失败"); } } finally { lock2.unlock(); } //随机休眠,避免出现活锁 try { Thread.sleep(r.nextInt(10)); } catch (Exception e) { e.printStackTrace(); } } }).start(); } }
结果:
线程:Thread-0 获取锁 lock1 成功 线程:Thread-1 获取锁 lock2 成功 线程:Thread-1 获取锁 lock1 失败 线程:Thread-1 获取锁 lock2 成功 线程:Thread-0 获取锁 lock2 失败 线程:Thread-1 获取锁 lock1 成功 线程:Thread-0 获取锁 lock1 成功 线程:Thread-0 获取锁 lock2 成功
3. 按照一定的顺序加锁,破坏 “循环等待” 条件实例
/** * 测试 按序申请资源,破坏 "循环等待" 条件 * @author ConstXiong * @date 2019-09-24 15:26:23 */ public class TestBreakLockCircleWait { private static Object res1 = new Object(); private static Object res2 = new Object(); public static void main(String[] args) { new Thread(() -> { Object first = res1; Object second = res2; //比较 res1 和 res2 的 hashCode,如果 res1 的 hashcode > res2,交换 first 和 second。保证 hashCode 小的对象先加锁 if (res1.hashCode() > res2.hashCode()) { first = res2; second = res1; } synchronized (first) { System.out.println("线程:" + Thread.currentThread().getName() + "获取资源 " + first + " 锁成功"); try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } synchronized(second) { System.out.println("线程:" + Thread.currentThread().getName() + "获取资源 " + second + " 锁成功"); } } }).start(); new Thread(() -> { Object first = res1; Object second = res2; //比较 res1 和 res2 的 hashCode,如果 res1 的 hashcode > res2,交换 first 和 second。保证 hashCode 小的对象先加锁 if (res1.hashCode() > res2.hashCode()) { first = res2; second = res1; } synchronized (first) { System.out.println("线程:" + Thread.currentThread().getName() + "获取资源 " + first + " 锁成功"); try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } synchronized(second) { System.out.println("线程:" + Thread.currentThread().getName() + "获取资源 " + second + " 锁成功"); } } }).start(); } }
结果:
线程:Thread-0获取资源 java.lang.Object@7447157c 锁成功 线程:Thread-0获取资源 java.lang.Object@7a80f45c 锁成功 线程:Thread-1获取资源 java.lang.Object@7447157c 锁成功 线程:Thread-1获取资源 java.lang.Object@7a80f45c 锁成功
以上,是Java面试题【怎么避免死锁】的参考答案。
输出,是最好的学习方法。
欢迎在评论区留下你的问题、笔记或知识点补充~
—end—