当前位置: 首页 > news >正文

Java多线程进阶-JUC之ReentrantLock与Callable

文章目录

  • Java多线程进阶:JUC之ReentrantLock与Callable
    • 一、`Callable`, `Future` 与 `FutureTask`:优雅地获取线程返回值
      • 1. `Runnable` 的局限性:无法带回结果的“信使”
      • 2. `Callable` 与 `Future`:定义任务与获取凭证
      • 3. 回顾:`wait/notify` 的传统实现方式
      • 4. `FutureTask<V>`:连接 `Callable` 与 `Thread` 的关键桥梁
      • 5. 代码实战:优雅地计算 1 到 1000 的和
    • 二、`ReentrantLock`:一个功能更强大的显式锁
      • 深入 `ReentrantLock` 的高级功能
        • 1. 可中断与可限时的锁等待
        • 2. 公平锁 vs 非公平锁
        • 3. `Condition` 对象:实现精准的线程唤醒
    • 本篇核心要点总结 (Key Takeaways)

Java多线程进阶:JUC之ReentrantLock与Callable

在前两篇笔记中,我们深入探讨了锁的底层策略以及 synchronized 的实现。从本篇笔记开始,我们将正式进入 java.util.concurrent (JUC) 的 常见类,去领略其提供的更强大、更灵活的并发工具。

本文将聚焦于两个核心组件:

  1. CallableFuture:一套优雅的异步任务与结果获取机制,完美解决了 Runnable 的固有缺陷。
  2. ReentrantLock:一个功能远超 synchronized 的显式锁。

掌握它们,将让你的并发编程能力提升到一个新的层次。


一、Callable, FutureFutureTask:优雅地获取线程返回值

1. Runnable 的局限性:无法带回结果的“信使”

在之前的学习中,我们知道通过 Runnable 接口可以定义一个任务,并交由线程去执行。但这存在一个很常见的问题:run() 方法没有返回值。

这就像我们派了一个信使(子线程)去远方执行一个复杂的计算任务,但他完成任务后,却没有办法把计算结果带回来告诉我们。如果需要获取结果,我们就不得不借助共享变量、wait/notify 等复杂的线程通信机制,这不仅麻烦,还容易出错。

为了解决这个问题,JUC 提供了一套更优雅的解决方案——CallableFuture

2. CallableFuture:定义任务与获取凭证

  • Callable<V> 接口:它像是一个增强版的 Runnable。它的核心方法 call() 不仅可以返回一个泛型结果 V,还能抛出异常。这解决了 Runnable 的两大痛点。

    public interface Callable<V> {V call() throws Exception;
    }
    
  • Future<V> 接口:如果说 Callable 是“会产生结果的任务”,那么 Future 就是“获取这个未来结果的凭证”。当你把一个 Callable 任务提交给线程池或线程去执行时,你会立刻得到一个 Future 对象。这个对象代表着一个未来的、尚未完成的计算结果。

    你可以通过这个 Future 凭证,在未来的任意时刻:

    • V get(): 阻塞式地等待,直到任务完成并返回结果。
    • V get(long timeout, TimeUnit unit): 限时等待,如果在指定时间内任务还未完成,就抛出 TimeoutException
    • boolean isDone(): 查询任务是否已经完成。
    • boolean cancel(boolean mayInterruptIfRunning): 尝试取消任务的执行。

3. 回顾:wait/notify 的传统实现方式

为了更好地理解 Callable 的价值,我们先回顾一下,如果只用 Runnablewait/notify 机制,要实现“获取子线程计算结果”这个需求,代码会是怎样的。

需求: 创建一个子线程计算 1 + 2 + … + 1000 的和,主线程等待计算完成后获取并打印结果。

实现思路:

  1. 创建一个共享对象 Result,用于存储计算结果 sum 和一个用于同步的锁对象 lock
  2. main 方法中创建 Result 实例,并创建一个子线程 t
  3. 子线程 t 负责执行累加计算。计算完成后,它会获取 result.lock 锁,将结果存入 result.sum,然后调用 notify() 唤醒可能正在等待的主线程。
  4. 主线程启动子线程后,会立刻尝试获取 result.lock 锁。进入同步块后,它会使用 while 循环来检查 result.sum 是否已经被计算出来。如果还没有(sum 仍为0),则调用 wait() 方法,释放锁并进入等待状态。
  5. 当子线程调用 notify() 后,主线程被唤醒,再次检查 while 条件。此时条件不满足,循环退出,主线程打印最终结果。

代码示例:

// 辅助类,用于封装结果和锁
static class Result {public int sum = 0;public final Object lock = new Object();
}public static void main(String[] args) throws InterruptedException {Result result = new Result();Thread t = new Thread(new Runnable() {@Overridepublic void run() {int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}// 计算完成后,锁定共享对象并更新结果synchronized (result.lock) {result.sum = sum;// 通知正在等待的主线程result.lock.notify();}}});t.start();// 主线程锁定同一个对象synchronized (result.lock) {// 使用 while 是为了防止“虚假唤醒”// 并且,如果子线程执行得非常快,在主线程wait之前就已完成,// 那么这个循环条件会直接不满足,避免了主线程不必要的等待。while (result.sum == 0) {result.lock.wait();}System.out.println("计算结果是: " + result.sum);}
}

小思考:传统方式的痛点

正如你所看到的,仅仅是为了在线程间传递一个简单的整数结果,我们就不得不:

  • 额外定义一个 Result 类。
  • 小心翼翼地使用 synchronizedwaitnotify
  • 处理复杂的线程状态同步问题,比如wait的时机和虚假唤醒。

整个过程代码繁琐,逻辑复杂,且极易出错。这正是 CallableFuture 致力于解决的问题。

4. FutureTask<V>:连接 CallableThread 的关键桥梁

现在问题来了:Thread 类的构造方法只接受 Runnable 对象,并不直接支持 Callable。那么如何让一个 Thread 去执行 Callable 任务呢?

答案就是使用 FutureTaskFutureTask 是一个非常巧妙的适配器类,它扮演着双重角色:

  1. 它是一个 RunnableFutureTask 实现了 Runnable 接口,所以它可以被直接传入 Thread 的构造函数中。
  2. 它也是一个 FutureFutureTask 也实现了 Future 接口,所以它能像凭证一样,被用来获取任务的执行结果。

它的工作流程是:

  1. 用你的 Callable 任务来构造一个 FutureTask 对象。
  2. 将这个 FutureTask 对象当作 Runnable 传递给一个 Thread
  3. 启动线程,线程会执行 FutureTaskrun() 方法,而 run() 方法内部会去调用你传入的 Callablecall() 方法。
  4. 在主线程中,你可以通过 FutureTask 对象自身的 get() 方法来等待并获取最终结果。

一个比喻:麻辣烫店的取餐小票

我们可以把这套机制想象成在一家现代化的麻辣烫店点餐:

  • Callable 任务:就是你的点餐需求——“一份微辣、多加麻酱的麻辣烫”。
  • FutureTask 对象:就是你下单后,前台递给你的那张取餐小票。这张小票既是厨房(子线程)执行任务的依据(因为它包含了点餐信息,像个Runnable),也是你将来取餐的唯一凭证(因为它能兑换结果,像个Future)。
  • Thread:就是后厨里那位专门为你制作麻辣烫的厨师。他从你手里接过小票,然后开始工作。
  • futureTask.get():就是你拿着小票,站在出餐口等待叫号。这个过程是阻塞的,在你的麻辣烫做好之前,你只能在这里等。如果厨师不小心做坏了(任务抛出异常),他会通过窗口告诉你(get()方法会将异常抛出)。
  • futureTask.isDone():你不想干等,于是每隔几分钟就抬头看一眼电子屏幕,查询你的取餐号是否出现在“已完成”列表里
  • futureTask.cancel():你突然不想吃了,于是拿着小票去前台说:“麻烦帮我取消订单”。

5. 代码实战:优雅地计算 1 到 1000 的和

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class CallableExample {public static void main(String[] args) throws ExecutionException, InterruptedException {// 1. 定义一个 Callable 任务,它描述了“计算 1-1000 的和”这个操作,并能返回一个 Integer 结果。Callable<Integer> callable = () -> {System.out.println("子线程开始计算...");int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}Thread.sleep(1000); // 模拟耗时计算System.out.println("子线程计算完成!");return sum;};// 2. 将 Callable 任务包装成一个 FutureTask 对象。//    FutureTask 既是任务(Runnable),也是结果凭证(Future)。FutureTask<Integer> futureTask = new FutureTask<>(callable);// 3. 创建并启动线程。将 FutureTask 作为任务交给 Thread。Thread t = new Thread(futureTask);t.start();System.out.println("主线程已提交任务,正在做其他事情...");// 4. 在主线程中,调用 get() 方法获取子线程的计算结果。//    这个方法会阻塞,直到子线程的 call() 方法执行完毕并返回结果。Integer result = futureTask.get();System.out.println("主线程获取到结果:" + result);}
}

通过 CallableFutureFutureTask 的组合,我们无需手动编写复杂的 wait/notify 代码,就能轻松实现线程间的异步协作和结果传递,代码逻辑也变得更加清晰直观。


二、ReentrantLock:一个功能更强大的显式锁

ReentrantLock 是一个可重入互斥锁,和 synchronized 定位类似,但它是一个纯粹的Java类,提供了更丰富、更灵活的功能。

“Reentrant” 这个单词的原意就是 “可重⼊”

基本用法:

ReentrantLock lock = new ReentrantLock();
// 必须在finally块中解锁,这是至关重要的规范!
lock.lock();
try {// 受保护的业务逻辑
} finally {lock.unlock();
}

ReentrantLocksynchronized 的核心区别:

特性synchronizedReentrantLock
实现层面JVM关键字,由JVM层面实现JDK库中的一个类,由API层面实现
锁的释放自动释放(退出同步块时)必须手动调用 unlock(),否则会造成死锁
等待机制获取不到锁会死等提供了 tryLock(),可限时等待或立即返回,避免无限期阻塞
公平性非公平锁默认为非公平,但可创建公平锁 new ReentrantLock(true)
唤醒机制配合 wait/notify/notifyAll,随机或全部唤醒配合 Condition 对象,可实现分组唤醒,精确控制

深入 ReentrantLock 的高级功能

synchronized 像是一把简单可靠的自动挡汽车钥匙,而 ReentrantLock 则更像是一辆手动挡性能跑车,它赋予了驾驶员(程序员)更大的控制权。下面我们来深入了解一下表格中提到的几个核心高级功能。

1. 可中断与可限时的锁等待

synchronized 在获取锁时,如果锁被占用,线程会进入阻塞状态,并且这个等待过程是无法被中断的,只能死等。ReentrantLock 提供了更灵活的选择:

  • tryLock(): 尝试获取锁,如果锁未被占用,则立即获取成功并返回 true;如果锁已被占用,则不会等待,立即返回 false。这在某些“尝试一下,不行就算了”的场景中非常有用。
  • tryLock(long timeout, TimeUnit unit): tryLock 的限时版本。线程会在指定的时间内尝试获取锁,如果超时仍未获取到,则返回 false。这可以有效避免线程无限期等待,防止死锁。
  • lockInterruptibly(): 这是 ReentrantLock 的一个重要特性。调用此方法的线程在等待获取锁的过程中,可以响应中断请求(例如,另一个线程调用了它的 interrupt() 方法)。这使得我们可以设计出能够取消或提前终止的锁等待逻辑,而 synchronized 是做不到的。
2. 公平锁 vs 非公平锁
  • 非公平锁 (默认): 当锁被释放时,所有等待的线程会蜂拥而上进行争抢,谁抢到就是谁的。这种方式吞吐量较大,因为减少了线程切换的开销,但可能导致某些线程长时间获取不到锁,即“饥饿”现象。synchronized 就是一种非公平锁。
  • 公平锁: new ReentrantLock(true)。它会维护一个等待队列,当锁被释放时,会从队列头部唤醒等待时间最长的线程。这种方式保证了先来后到,非常公平,但由于需要频繁地进行线程上下文切换,其性能通常低于非公平锁。

小思考:如何选择公平性?

除非你有非常明确的业务需求,需要保证所有线程都能最终获得锁(例如,防止资源饥饿),否则在绝大多数追求性能的场景下,默认的非公平锁是更好的选择

3. Condition 对象:实现精准的线程唤醒

synchronized 配合的 wait/notify/notifyAll 机制有两个痛点:

  1. notify 是随机唤醒一个等待线程,无法指定唤醒哪一个。
  2. notifyAll 会唤醒所有等待线程,造成“惊群效应”,即大量被唤醒的线程再次尝试获取锁,导致激烈的竞争和不必要的上下文切换。

ReentrantLock 通过 Condition 对象解决了这个问题。一个 Lock 可以创建多个 Condition 对象 (lock.newCondition()),每个 Condition 都可以看作一个独立的等待队列。

  • condition.await(): 类似于 object.wait(),使当前线程进入等待状态,并释放锁。
  • condition.signal(): 类似于 object.notify(),唤醒该 Condition 等待队列中的一个线程。
  • condition.signalAll(): 类似于 object.notifyAll(),唤醒该 Condition 等待队列中的所有线程。

通过为不同的业务逻辑创建不同的 Condition 对象,我们可以实现“分组唤醒”或“精准唤醒”,只唤醒那些需要被唤醒的线程,极大地提升了效率和代码的可控性。

如何选择?

  • 在锁竞争不激烈、功能简单的场景下, 优先使用 synchronized, 因为它语法简洁,不易出错,且JVM对其有持续的优化。
  • 在需要高级功能(如公平锁、可中断的锁获取、限时等待、精确唤醒)或锁竞争非常激烈的场景下, 使用 ReentrantLock 能提供更强的控制力和性能。

本篇核心要点总结 (Key Takeaways)

  • CallableFuture是对Runnable的增强Callable允许任务返回结果并抛出异常,而Future则提供了一个获取这个“未来”结果的凭证,极大地简化了异步编程。
  • FutureTask是关键适配器:它同时扮演RunnableFuture的角色,是连接Callable任务与Thread执行实体的桥梁。
  • ReentrantLocksynchronized的进阶版:它提供了更丰富的功能,如可中断的锁等待、限时等待、公平性选择和精确唤醒,适用于更复杂的并发场景。
  • lock()unlock()必须配对使用:使用ReentrantLock时,必须在finally块中调用unlock()来释放锁,这是保证程序健壮性的铁律。
    RunnableFuture的角色,是连接Callable任务与Thread`执行实体的桥梁。
  • ReentrantLocksynchronized的进阶版:它提供了更丰富的功能,如可中断的锁等待、限时等待、公平性选择和精确唤醒,适用于更复杂的并发场景。
  • lock()unlock()必须配对使用:使用ReentrantLock时,必须在finally块中调用unlock()来释放锁,这是保证程序健壮性的铁律。
http://www.dtcms.com/a/335445.html

相关文章:

  • Oracle algorithm的含义
  • 【牛客刷题】01字符串按递增长度截取并转换为十进制数值
  • 26. 值传递和引用传递的区别的什么?为什么说Java中只有值传递
  • 告别“测试滞后”:AI实时测试工具在敏捷开发中的落地经验
  • 【JavaEE】多线程 -- 单例模式
  • 基于Python的情感分析与情绪识别技术深度解析
  • 锂电池SOH预测 | Matlab基于KPCA-PLO-Transformer-LSTM的的锂电池健康状态估计(锂电池SOH预测),附锂电池最新文章汇集
  • CVPR2 2025丨大模型创新技巧:文档+语音+视频“大模型三件套”
  • 音频分类标注工具
  • 91.解码方法
  • GaussDB 数据库架构师修炼(十三)安全管理(5)-全密态数据库
  • 17.5 展示购物车缩略信息
  • JMeter(进阶篇)
  • 3D打印——给开发板做外壳
  • 蓝凌EKP产品:JSP 性能优化和 JSTL/EL要点检查列表
  • Trae 辅助下的 uni-app 跨端小程序工程化开发实践分享
  • Docker之自定义jkd镜像上传阿里云
  • Spring AI 集成阿里云百炼平台
  • vscode无法检测到typescript环境解决办法
  • SpringCloud 03 负载均衡
  • 向量数据库基础和实践 (Faiss)
  • QT 基础聊天应用项目文档
  • Flutter vs Pygame 桌面应用开发对比分析
  • Android原生(Kotlin)与Flutter混合开发 - 设备控制与状态同步解决方案
  • 安卓开发者自学鸿蒙开发2页面高级技巧
  • 第一阶段总结:你的第一个3D网页
  • 【牛客刷题】成绩统计与发短信问题详解
  • OpenMemory MCP发布!AI记忆本地共享,Claude、Cursor一键同步效率翻倍!
  • 【FreeRTOS】刨根问底6: 应该如何防止任务栈溢出?
  • JavaScript性能优化实战(四):资源加载优化