基于输入参数实现可变数量锁的Java两阶段死锁预防算法问询
嘿,这个问题我刚好有经验,咱们一步步来解决,顺便聊聊两阶段锁的优化思路~
首先,我们需要解决两个关键问题:从输入文件解析进程和资源请求,以及实现可变数量锁的尝试-回滚-重试逻辑。
1. 调整Resource类的可访问性
你的Resource类里的ReentrantLock是私有变量,线程无法直接调用tryLock,所以我们需要添加一个getter方法(符合封装原则):
class Resource { private int resourceId; private ReentrantLock theLock; public Resource(int resourceId) { this.resourceId = resourceId; this.theLock = new ReentrantLock(); } // 提供锁的访问入口 public ReentrantLock getLock() { return theLock; } // 可选:封装资源使用逻辑,避免手动处理锁遗漏 public void use(Runnable action) { theLock.lock(); try { action.run(); } finally { theLock.unlock(); } } }
2. 实现进程线程的锁尝试逻辑
每个进程对应一个线程,核心逻辑是:循环尝试获取所有需要的锁,中途失败则释放已持有的锁并重试;成功获取后执行业务逻辑,最后逆序释放所有锁。
class ProcessThread extends Thread { private List<Integer> sortedResourceIds; // 排序后的资源ID,优化冲突 private Map<Integer, Resource> resourceMap; public ProcessThread(List<Integer> requestedResourceIds, Map<Integer, Resource> resourceMap) { // 关键优化:对资源ID排序,所有进程按相同顺序获取锁,打破循环等待条件 this.sortedResourceIds = requestedResourceIds.stream() .sorted() .collect(Collectors.toList()); this.resourceMap = resourceMap; } @Override public void run() { List<Resource> acquiredResources = new ArrayList<>(); boolean allLocksAcquired = false; while (!allLocksAcquired && !Thread.currentThread().isInterrupted()) { acquiredResources.clear(); allLocksAcquired = true; // 第一阶段:尝试获取所有锁 for (Integer id : sortedResourceIds) { Resource res = resourceMap.get(id); // 无参tryLock立即返回,也可以加超时:tryLock(100, TimeUnit.MILLISECONDS) if (res.getLock().tryLock()) { acquiredResources.add(res); } else { allLocksAcquired = false; break; } } if (!allLocksAcquired) { // 释放已获取的锁,逆序释放更规范(顺序其实不影响,但养成好习惯) for (int i = acquiredResources.size() - 1; i >= 0; i--) { acquiredResources.get(i).getLock().unlock(); } // 随机等待避免忙等,减少CPU占用 try { Thread.sleep(100 + new Random().nextInt(200)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } System.out.printf("线程%d获取锁失败,释放已持有的锁,稍后重试%n", Thread.currentThread().getId()); } } if (Thread.currentThread().isInterrupted()) { return; } try { // 第二阶段:执行业务逻辑(使用资源) System.out.printf("线程%d成功获取所有锁,开始执行操作%n", Thread.currentThread().getId()); // 模拟资源使用时间 Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 释放所有锁,逆序释放 for (int i = acquiredResources.size() - 1; i >= 0; i--) { acquiredResources.get(i).getLock().unlock(); } System.out.printf("线程%d释放所有锁,操作完成%n", Thread.currentThread().getId()); } } }
3. 解析输入文件
按照你指定的格式读取文件,解析出进程数、资源数和每个进程的资源请求列表:
public class TwoPhaseLockDemo { public static void main(String[] args) throws IOException { // 读取输入文件,按行解析更符合你的格式要求 List<String> lines = Files.readAllLines(Paths.get("resource_requests.txt")); int processCount = Integer.parseInt(lines.get(0).trim()); int resourceCount = Integer.parseInt(lines.get(1).trim()); // 初始化资源池:资源ID从1到resourceCount Map<Integer, Resource> resourceMap = new HashMap<>(); for (int i = 1; i <= resourceCount; i++) { resourceMap.put(i, new Resource(i)); } // 解析每个进程的资源请求 List<List<Integer>> processRequests = new ArrayList<>(); for (int i = 2; i < 2 + processCount; i++) { String line = lines.get(i).trim(); if (line.isEmpty()) { processRequests.add(new ArrayList<>()); continue; } List<Integer> reqs = Arrays.stream(line.split("\\s+")) .map(Integer::parseInt) .collect(Collectors.toList()); processRequests.add(reqs); } // 启动所有进程线程 List<Thread> threads = new ArrayList<>(); for (List<Integer> reqs : processRequests) { Thread thread = new ProcessThread(reqs, resourceMap); threads.add(thread); thread.start(); } // 等待所有线程执行完毕 for (Thread thread : threads) { try { thread.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }
上面的代码已经满足你的需求,但还有几个可以优化的点:
1. 锁排序优化(必做)
我在代码里已经加入了资源ID排序的逻辑,这是两阶段锁预防死锁的关键优化:所有进程按相同的顺序请求资源,打破死锁的「循环等待」必要条件,能大幅减少重试次数和冲突概率。
2. 避免忙等的进阶方案
当前用Thread.sleep是简单有效的方式,但高并发场景下可以用Condition来实现精准通知:当某个锁被释放时,通知等待该锁的线程重试。不过这会增加复杂度,需要为每个资源维护等待队列,适合对性能要求极高的场景。
3. 超时与中断机制
在tryLock时添加超时时间(比如tryLock(500, TimeUnit.MILLISECONDS)),避免线程无限期等待;同时处理中断信号,保证线程能优雅退出。
4. 资源使用的封装
把资源的业务逻辑封装到Resource类的use方法中,避免在线程里直接操作锁,降低出错概率(比如忘记释放锁)。
5. 监控与日志
添加日志记录线程的锁操作(获取、释放、重试),方便调试和排查问题。比如用SLF4J框架记录详细的操作日志。
内容的提问来源于stack exchange,提问作者Khatevate

