并发 (Concurrency)
并发指的是程序能够同时处理多个任务,或者通过在这些任务之间快速切换来管理它们的能力。这在处理需要同时应对多个用户、任务或事件的应用程序中尤其重要。
在 Java 中,并发主要依赖于线程 (threads)。
线程是 Java 应用程序中最小的独立执行单元。
Java 透过同时运行多个线程来支持并发,每个线程都独立于其他线程执行。
示例: 创建并运行一个基础的 Java 线程:
class MyTask implements Runnable {
@Override
public void run() {
// 打印当前线程的名称
System.out.println("正在并发运行任务: " + Thread.currentThread().getName());
}
}
public class ThreadExample {
public static void main(String[] args) {
// 创建两个任务
Thread thread1 = new Thread(new MyTask());
Thread thread2 = new Thread(new MyTask());
// 启动线程
thread1.start();
thread2.start();
// 主线程继续执行
System.out.println("主线程: " + Thread.currentThread().getName());
}
}
输出可能会像这样(顺序不保证):
主线程: main
正在并发运行任务: Thread-0
正在并发运行任务: Thread-1
并行 (Parallelism)
并行指的是真正地在同一时刻执行多个任务。具体来说,它意味着利用多个 CPU 核心或处理器,在物理上同时运行多个任务或指令。
与并发不同,并行不仅仅是管理多个任务——它是为了提升性能而明确地在同一瞬间执行多个任务。
示例: 想象一下对一个巨大的数据集进行排序。通过并行处理,你可以将数据集分割成多个小块,然后在多个 CPU 核心上同时对每个小块进行排序。
核心区别:
• 并发 (Concurrency) 是关于通过交错执行来处理多个任务,允许你同时管理许多任务(但不一定是在完全相同的瞬间执行)。
• 并行 (Parallelism) 则涉及在完全相同的时刻执行多个任务(通常需要多个 CPU 核心)。
餐厅厨房的比喻
想象一个餐厅厨房:
-
• 1 个厨师 = 1 个 CPU 核心
-
• 4 个厨师 = 4 个 CPU 核心
-
• 一长串的菜单订单 = 需要执行的任务
并发 (1 个厨师,很多道菜)
一个厨师通过在不同菜品之间来回切换来同时处理多道菜。
他可能先开始煮意面,然后在意面煮着的时候去切蔬菜,然后再回到意面那边。
在任何一个瞬间,他只在做一件事情,但因为他在任务间交错进行,所以看起来像是在同时做很多事。
这就是并发。
并行 (4 个厨师,同时做 4 道菜)
每个厨师在同一时间各自做一道菜。
一个厨师做意面,另一个做牛排,第三个做沙拉,第四个做汤。
所有人都在同一时刻工作。
这就是并行。
现在,用 Java 的术语来说:
// 创建一个包含4个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(4);
这意味着:
-
• 你创建了一个有 **4 个线程(厨师)**的池子。
-
• 如果你的电脑有 4 个或更多 CPU 核心,那么这 4 个线程就可以真正地并行运行 → 这是并行 ✅
-
• 如果你的电脑只有 1 个核心,那么这 4 个线程将轮流交错执行 → 这只是并发 ✅
ThreadPool, ExecutorService 和 Future
手动管理线程很快就会变得复杂且容易出错。为了简化并发编程并提高效率,Java 提供了强大的工具,如线程池 (ThreadPools)、ExecutorService
框架以及 Future
接口。
线程池 (ThreadPool): 高效的线程管理
为每个新任务都创建一个新线程的成本是很高的——会消耗大量的内存和 CPU 开销。线程池通过维护一个可复用的线程集合来解决这个问题,这些线程可以随时用来执行多个任务。
-
• 线程池的好处:
-
• 减少了因创建和销毁线程而产生的开销。
-
• 通过复用线程来限制资源消耗。
-
• 通过将任务放入队列来高效地管理工作负载。
-
ExecutorService: 灵活的线程池管理器
ExecutorService
是 Java 对线程池管理器的实现。它通过自动处理线程的创建、复用和调度,极大地简化了线程管理。
-
• 创建一个 ExecutorService
Java 提供了内置的静态工厂方法来轻松创建不同类型的线程池:- • 固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(10);
- • 缓存线程池: 根据工作负载动态调整线程数量。
ExecutorService executor = Executors.newCachedThreadPool();
- • 定时任务线程池: 在给定延迟后或周期性地执行任务。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
- • 单线程执行器: 确保任务按顺序依次执行。
ExecutorService executor = Executors.newSingleThreadExecutor();
- • 固定大小的线程池:
动手实践:从阻塞式调用到异步编程
在本节中,我们将通过一个动手示例,来探讨 Java 中默认的同步阻塞式调用是如何工作的,以及如何用 Future
和 CompletableFuture
将其改进为异步非阻塞。我们将使用两个简单的服务:一个用于获取电影,另一个用于获取评分。
示例代码 (初始状态):
// Movie.java - 代表电影对象
public class Movie {
private String title;
private Rating rating;
// 构造函数、getter 和 setter 省略...
}
// Rating.java - 代表电影评分
public class Rating {
private int movieId;
private int stars;
// 构造函数、getter 和 setter 省略...
}
// MovieService.java - 模拟获取电影(带有人为延迟)
public class MovieService {
public static Movie getMovie() {
Utils.delay(); // 模拟慢服务
return new Movie("一部情感电影", null);
}
}
// RatingService.java - 模拟获取评分(带有人为延迟)
public class RatingService {
public static Rating getRating() {
Utils.delay(); // 模拟慢服务
return new Rating(5, 5);
}
}
// Utils.java - 包含一个模拟延迟的静态方法
public class Utils {
public static void delay() {
try {
Thread.sleep(1000); // 延迟1秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
System.err.println("线程被中断: " + e.getMessage());
}
}
}
// 主程序 - 阻塞式调用
public class BlockingExample {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
// 1. 调用 MovieService,主线程阻塞等待1秒
Movie movie = MovieService.getMovie();
System.out.println("获取到电影...");
// 2. 调用 RatingService,主线程再次阻塞等待1秒
Rating rating = RatingService.getRating();
System.out.println("获取到评分...");
movie.setRating(rating);
System.out.println("电影: " + movie.getTitle() + ", 评分: " + movie.getRating().getStars());
long endTime = System.currentTimeMillis();
System.out.println("总耗时: " + (endTime - startTime) + " 毫秒"); // 大约 2000 毫秒
}
}
在上面的阻塞式示例中,获取电影和获取评分是串行执行的,总耗时是两者之和(约2秒)。
Future
在 Java 5 中引入的 Future
接口是向异步编程迈出的重要一步——它允许使用 ExecutorService
异步地执行任务。然而,Future
有其显著的局限性:它的 get()
方法本质上是阻塞的,它缺乏对任务组合的内置支持,并且不容易实现回调驱动的编程。
通过 Future
,我们可以将任务提交到线程池中,让获取电影和获取评分这两个服务调用并发执行。然后,我们使用 future.get()
等待两个结果。
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(2);
long startTime = System.currentTimeMillis();
// 并行提交两个任务
Future<Movie> movieFuture = executor.submit(MovieService::getMovie);
Future<Rating> ratingFuture = executor.submit(RatingService::getRating);
System.out.println("任务已提交...");
// 等待两个结果(仅当结果尚未就绪时才会阻塞)
Movie movie = movieFuture.get(); // 如果任务仍在运行,这里会等待
System.out.println("获取到电影 (耗时约1秒)...");
Rating rating = ratingFuture.get(); // 如果任务仍在运行,这里会等待
System.out.println("获取到评分 (总耗时仍约1秒)...");
movie.setRating(rating);
System.out.println("电影: " + movie.getTitle() + ", 评分: " + movie.getRating().getStars());
executor.shutdown();
long endTime = System.currentTimeMillis();
System.out.println("总耗时: " + (endTime - startTime) + " 毫秒"); // 大约 1000 毫秒
}
}
submit()
方法在线程池的独立线程中异步运行这两个任务。
movieFuture.get()
和 ratingFuture.get()
仅在结果尚未就绪时才会阻塞。
因为这两个耗时1秒的任务是并行运行的,所以总执行时间从大约2秒减少到了大约1秒。
CompletableFuture
在 Java 8 中引入的 CompletableFuture
是一个功能极其强大的抽象,它允许开发者以一种干净、流式的方式处理异步计算。
其核心思想是,CompletableFuture
代表一个承诺 (promise):一个可能在未来某个时间点才会产生的结果。但与旧的 Future
不同,它提供了一套丰富的 API,使得我们能够:
-
• 异步运行任务。
-
• 链式调用和组合多个异步操作。
-
• 在结果就绪时对其应用转换。
-
• 以声明式的风格处理异常。
-
• 所有这些操作都无需阻塞主线程。
这种模型非常适合诸如调用外部 API、并行处理数据、协调多个相互依赖的任务,或构建非阻塞微服务等场景。
import java.util.concurrent.*;
public class CompletableFutureExample {
public static void main(String[] args) throws InterruptedException {
long startTime = System.currentTimeMillis();
// 异步获取电影
CompletableFuture<Movie> movieFuture = CompletableFuture.supplyAsync(MovieService::getMovie);
// 异步获取评分
CompletableFuture<Rating> ratingFuture = CompletableFuture.supplyAsync(RatingService::getRating);
System.out.println("异步任务已启动...");
// 定义当两个 Future 都完成时要执行的操作
movieFuture
.thenCombine(ratingFuture, (movie, rating) -> { // 组合结果
movie.setRating(rating);
return movie;
})
.thenAccept(movie -> { // 消费最终结果
System.out.println("🎬 电影: " + movie.getTitle() + ", ⭐ 评分: " + movie.getRating().getStars());
long endTime = System.currentTimeMillis();
System.out.println("总耗时: " + (endTime - startTime) + " 毫秒"); // 大约 1000 毫秒
})
.exceptionally(ex -> { // 统一处理任何步骤中发生的异常
System.out.println("❌ 发生错误: " + ex.getMessage());
return null; // exceptionally 需要一个返回值
});
System.out.println("主线程可以继续做其他事情...");
Thread.sleep(2000); // 阻塞主线程以等待异步任务完成,以便观察输出
}
}
-
•
.supplyAsync(Supplier<U> supplier)
— 启动一个异步任务
此方法异步地运行一个Supplier
(提供者),默认使用公共的ForkJoinPool
线程池。 -
•
.thenCombine(CompletionStage<V> other, BiFunction<T,V,U> fn)
— 当两个 Future 都完成时,组合它们的结果
当你需要等待两个独立的异步任务都完成后,再将它们的结果合并处理时,使用此方法。 -
•
.thenApply(Function<T,U> fn)
— 转换结果
一旦你的异步结果就绪,你可以对其进行转换(比如映射数据)。 -
•
.exceptionally(Function<Throwable,? extends T> fn)
— 优雅地处理错误
如果异步流程中任何地方出错了(例如,API 调用失败),你可以提供一个回退逻辑。 -
•
.thenAccept(Consumer<T> action)
— 消费结果(无返回值)
当所有操作都完成,并且你对最终结果感到满意时,用此方法来消费结果。
其他有用的 CompletableFuture
方法
- 1.
.thenRun(Runnable action)
在前一个阶段完成后运行一个Runnable
任务,但它不消费结果,也不产生新结果。CompletableFuture .supplyAsync(MovieService::getMovie) .thenRun(() -> System.out.println("✅ 电影获取任务已完成!"));
- 2.
.thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
将一个 Future 的结果“扁平化地”链接到另一个 Future。当第二个异步操作依赖于第一个异步操作的结果时,这个方法非常有用。// 异步获取电影,然后根据电影ID异步获取评分 // CompletableFuture<Movie> movieWithRating = CompletableFuture // .supplyAsync(MovieService::getMovie) // 1. 获取电影 // .thenCompose(movie -> // 2. movie 是第一步的结果 // CompletableFuture.supplyAsync(() -> RatingService.getRatingForMovie(movie.getId())) // 3. 启动第二个异步任务 // .thenApply(rating -> { // 4. 拿到评分后 // movie.setRating(rating); // 5. 设置评分 // return movie; // 6. 返回最终带评分的电影 // }));
- 3.
.handle((result, exception) -> {...})
在一个地方同时处理正常结果和异常。CompletableFuture<Movie> future = CompletableFuture .supplyAsync(MovieService::getMovie) .handle((movie, ex) -> { // movie 是结果,ex 是异常 if (ex != null) { // 如果发生了异常 System.out.println("发生错误: " + ex.getMessage()); return new Movie("不可用", new Rating(0, 0)); // 返回一个默认的电影对象 } return movie; // 如果没有异常,正常返回电影对象 });
- 4.
.complete(T value)
手动完成一个CompletableFuture
,并设置其结果。CompletableFuture<String> future = new CompletableFuture<>(); // ... 在某个时刻,从另一个线程或回调中 future.complete("手动完成的结果");
- 5.
.orTimeout(long timeout, TimeUnit unit)
(Java 9+)
如果 Future 在指定的超时时间内没有完成,则会使其失败并抛出TimeoutException
。// CompletableFuture<String> future = CompletableFuture // .supplyAsync(() -> { // Utils.delay(2000); // 模拟一个2秒的延迟 // return "Hello"; // }) // .orTimeout(1, TimeUnit.SECONDS); // 设置1秒超时,这将会失败
- 6.
.completeOnTimeout(T value, long timeout, TimeUnit unit)
(Java 9+)
如果任务在指定的超时时间内没有完成,则使用一个备用值来完成 Future。// CompletableFuture<String> future = CompletableFuture // .supplyAsync(() -> { // Utils.delay(2000); // 模拟2秒延迟 // return "真实响应"; // }) // .completeOnTimeout("默认响应", 1, TimeUnit.SECONDS); // 1秒后超时,会返回 "默认响应"
-
7.
allOf(...)
&anyOf(...)
- •
CompletableFuture.allOf(...)
等待所有给定的 Future 都完成。// CompletableFuture<Void> all = CompletableFuture.allOf(movieFuture, ratingFuture, anotherFuture);
- •
CompletableFuture.anyOf(...)
当给定的 Future 中任何一个完成时,就会返回(“先到先得”)。// CompletableFuture<Object> any = CompletableFuture.anyOf(futureFromSource1, futureFromSource2);
-
•
allOf
: 当你需要所有结果都准备好之后才能继续时使用。 -
•
anyOf
: 当你只需要最快返回的那个结果,并且可以忽略其他结果时使用。
- •