ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java进阶篇之ReentrantLock:给等待设置期限,让取消及时生效

Java进阶篇之ReentrantLock:给等待设置期限,让取消及时生效 前一篇讨论了ConcurrentHashMap怎样协调共享数据。如果一次操作需要同时维护多个字段或者需要明确控制等待时间就要重新审视锁的使用方式。之前的《Java进阶篇之同步与锁》介绍过ReentrantLock的基本加锁与释放。今天继续往下走请求已经超时线程还应该排队吗任务被取消等待锁的线程能否及时离开没有拿到锁finally里还能调用unlock吗本文使用Java 21围绕等待、取消和释放展开不涉及分布式锁。一、先给等待选一种策略ReentrantLock提供了几种不同的获取方式。选择方法之前可以先问业务这次操作愿意等多久是否需要响应取消获取方式获取不到时等待期间能否通过中断退出lock()继续等待不会因中断而结束锁等待tryLock()立即返回false没有阻塞等待阶段tryLock(timeout, unit)最多等待指定时间超时返回false可以抛出InterruptedExceptionlockInterruptibly()等待获取可以抛出InterruptedException这里的“最多等待”指锁获取的超时语义线程调度可能让实际返回时刻稍晚它也不限制拿到锁后业务代码的执行时长。方法语义以Java 21的ReentrantLock文档为准。例如一个接口总预算为800ms锁获取已经消耗300ms那么后面的数据库操作不能再无条件获得完整800ms。调用链预算需要向下传递给每一步单独设置相同超时可能累加出更长的总耗时。二、超时与中断分别解决什么问题超时表达“我只愿意等到这里”。获取失败后可以返回繁忙、使用已有结果或者交给上层决定是否重试。中断表达外部发来的协作式取消信号。使用lockInterruptibly或带超时的tryLock时等待线程可以通过异常路径结束当前尝试。中断不会自动回滚已经完成的业务修改。图里的门代表临界区入口。计时器归零和收到取消信号都可能让等待者离开已经进入临界区的线程仍需负责自己的释放与收尾。异常处理也要明确职责能够继续向上抛出时保留InterruptedException在Runnable等不允许抛出受检异常的边界通常恢复中断标记并退出本次任务避免悄悄吞掉取消请求。三、完整示例验证超时和取消下面用主线程持有同一把锁分别启动两个工作线程。主线程在它们结束之前不释放锁因此结果不依赖“碰巧先运行哪一个线程”。CountDownLatch只用于确认第二个线程已经启动不负责制造锁竞争。保存为LockWaitingDemo.javaimportjava.util.concurrent.CountDownLatch;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.ReentrantLock;publicclassLockWaitingDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ReentrantLocklocknewReentrantLock();lock.lock();try{ThreadtimednewThread(()-{try{if(!lock.tryLock(100,TimeUnit.MILLISECONDS)){System.out.println(timed: timeout);return;}try{System.out.println(timed: acquired);}finally{lock.unlock();}}catch(InterruptedExceptione){Thread.currentThread().interrupt();System.out.println(timed: cancelled);}});timed.start();timed.join();CountDownLatchstartednewCountDownLatch(1);ThreadcancellablenewThread(()-{started.countDown();try{lock.lockInterruptibly();try{System.out.println(cancel: acquired);}finally{lock.unlock();}}catch(InterruptedExceptione){Thread.currentThread().interrupt();System.out.println(cancel: interruptedThread.currentThread().isInterrupted());}});cancellable.start();started.await();cancellable.interrupt();cancellable.join();}finally{lock.unlock();}System.out.println(main: lockedlock.isLocked());}}运行命令javac LockWaitingDemo.javajavaLockWaitingDemo输出timed: timeout cancel: interruptedtrue main: lockedfalse第一行来自等待超时。第二个线程可能在调用lockInterruptibly前就收到中断也可能已经开始等待这两种时序都会走取消路径。catch恢复标记后isInterrupted返回true。这里让主线程持锁并join工作线程是为了构造有限等待的演示条件。不要把第二个线程改为lock后照搬到业务它会等待主线程释放主线程又等待它结束从而互相卡住。四、finally必须对应一次成功获取最容易写错的是下面这种结构// 错误示意获取阶段抛出异常也会执行unlocktry{lock.lockInterruptibly();// 业务操作}finally{lock.unlock();}在尚未持有该锁的情况下被中断finally会尝试释放不属于当前线程的锁可能让IllegalMonitorStateException掩盖原来的取消原因。更清楚的结构是先成功获取再进入负责释放的trylock.lockInterruptibly();try{// 仅在拿到锁后执行}finally{lock.unlock();}带超时的版本同样先判断返回值。返回false的分支直接结束成功分支才配对unlock。这种写法把锁的所有权直接体现在代码结构里也符合Lock接口建议的释放模式。五、使用时再检查三个边界缩短持锁区间。把耗时日志、远程调用和可提前计算的内容移出临界区确实需要共同保护的状态则在锁内一起检查和修改。读取与修改的边界必须由业务不变量决定。取消后不要立即无限重试。如果上层已经取消任务重新排队只会让取消失去意义。超时重试也需要次数限制与剩余预算不能在循环里不断重置期限。重入也需要配对释放。同一线程可以再次获取ReentrantLock但每次成功获取都要对应一次unlock。为排查问题读取isLocked等状态可以辅助观察不应据此替代真正的获取操作观察结束后其他线程仍可能改变状态。六、 思维导图ReentrantLock等待控制获取策略立即尝试限时等待可中断等待退出原因超时返回false中断异常释放规则成功获取再进入tryfinally配对unlock业务边界传递剩余预算取消后结束任务七、总结总结要点等待策略要与业务预算一致。立即失败、限时等待和可中断等待分别适用于不同的调用需求。获取与释放应在结构上配对。先确认获取成功再进入finally负责的范围避免释放未持有的锁。取消处理需要贯穿调用链。中断到达之后线程要结束当前任务或向上交还处理权已有业务结果是否回滚需要另行设计。下一篇继续学习Condition线程怎样等待业务条件以及为什么await通常要放在while循环中。如果你觉得这篇文章对你有所帮助欢迎点赞、收藏、分享
返回列表