JAVA中JUC多线程并发编程
第一章 java中基础概念和CompletableFuture
文章目录
- JAVA中JUC多线程并发编程
- 一、线程基础知识
- 二、CompletableFuture
-
- [1.Callable 和 Future](#1.Callable 和 Future)
- 2.CompletableFuture
- 3.实际应用
一、线程基础知识
1.为什么多线程极其重要
硬件方面:摩尔定律失效
摩尔定律:
它是由英特尔创始人之一Gordon Moore(戈登·摩尔)提出来的。其内容为:
当价格不变时,集成电路上可容纳的元器件的数目约每隔18-24个月便会增加一倍,性能也将提升一倍。
换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。
可是从2003年开始CPU主频已经不再翻倍,而是采用多核而不是更快的主频。
摩尔定律失效。
在主频不再提高且核数在不断增加的情况下,要想让程序更快就要用到并行或并发编程。
2.java中的线程
private native void start0();
Java语言本身底层就是C++语言:
大概执行过程: java线程是通过start的方法启动执行的,主要内容在native方法start0中,Openjdk的写JNI一般是一一对应的,Thread.java对应的就是Thread.c,start0其实就是JVM_StartThread。此时查看源代码可以看到在jvm.h中找到了声明,jvm.cpp中有实现。再到thread.cpp,最后发现调用os::start_thread(thread) :意思是最终确实会调用操作系统的原生 API 来创建真正的系统线程。(可以看OpenJDK源码)
3.相关概念
进程 :是程序的⼀次执⾏,是系统进⾏资源分配的最小单位,每⼀个进程都有它⾃⼰的内存空间和系统资源
线程 :在同⼀个进程内⼜可以执⾏多个任务,⽽这每⼀个任务我们就可以看做是⼀个线程,是调度的最小单元,⼀个进程会有1个或多个线程的
管程:Monitor是管程模型在编程语言(如 Java)或操作系统中的具体实现。(通常称为监视器或对象锁)Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象,Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。具体体现在 synchronized 关键字和 Object 类的 wait/notify 方法上,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。
Java线程分为用户线程和守护线程 ,线程的daemon属性为true表示是守护线程,false表示是用户线程
用户线程 :是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程
守护线程:是系统的工作线程,它会完成这个程序需要完成的业务操作。当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出。设置守护线程,需要在start()方法之前进行
二、CompletableFuture
1.Callable 和 Future
Callable 和 Future 是 Java 并发编程中一对紧密协作的核心组件,它们共同解决了 Runnable 无法返回执行结果和处理检查型异常的局限。
关系:
当你将一个 Callable 任务提交给线程池(ExecutorService)后,线程池会立即返回一个 Future 对象。这个 Future 对象就像一个"取餐凭证",你可以在未来某个时刻用它来获取任务的执行结果。
Future 虽然解决了结果获取的问题,但它本身存在一些局限,比如不支持链式调用、难以组合多个任务等。
因此,Java 8 引入了 CompletableFuture,它是 Future 的增强版,提供了更强大的异步编程能力:
2.CompletableFuture
- 在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法。
- 它可能代表一个明确完成的Future,也有可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些函数或执行某些动作。
- 它实现了Future和CompletionStage接口
简单代码:无返回值
java
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"-----come in");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-----task is over");
});
System.out.println(future.get());
}
}
简单代码:有返回值
java
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ThreadLocalRandom.current().nextInt(100);
});
System.out.println(completableFuture.get());
}
}
常用方法
- 获得结果和触发计算:join()会阻塞当前线程,get()会阻塞当前线程complete()就是结束阻塞并get()获取设置的complete里面的值.
- 对计算结果进行处理:thenApply()串行,handle()有异常也可以往下一步走,根据带的异常参数可以进一步处理
- 对计算结果进行消费:thenAccept()
- 对计算速度进行选用:applyToEither()谁快用谁
- 对计算结果进行合并:thenCombine()两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine
注意:还有其他好多方法,不一一列举了,xxxxAsync主要区别是用那个线程
3.实际应用
它的核心价值在于 :将串行执行的任务改为并行执行,从而大幅降低接口响应时间;或者将复杂的业务逻辑通过链式调用变得清晰易读。但是也要注意oom
聚合多个独立数据源(并行调用)
这是最经典、收益最明显的场景。
场景描述:在一个微服务架构的系统中,首页或详情页往往需要聚合多个下游服务的数据。
传统做法(串行):先查用户信息 -> 再查订单 -> 再查积分。总耗时 = 三者之和。
优化做法(并行):同时发起三个查询,最后等待结果聚合。总耗时 ≈ 最慢的那个查询。
java
// 1. 异步发起三个独立查询
CompletableFuture<UserInfo> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(userId), executor);
CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(() -> orderService.getOrders(userId), executor);
CompletableFuture<Integer> pointsFuture = CompletableFuture.supplyAsync(() -> pointsService.getPoints(userId), executor);
// 2. 等待所有任务完成并组合结果
CompletableFuture<UserProfile> result = CompletableFuture.allOf(userFuture, ordersFuture, pointsFuture)
.thenApply(v -> {
UserProfile profile = new UserProfile();
profile.setUser(userFuture.join()); // 此时join不会阻塞,因为已经确定完成了
profile.setOrders(ordersFuture.join());
profile.setPoints(pointsFuture.join());
return profile;
});
异步任务链式处理(流水线)
场景描述:业务逻辑有明显的先后依赖关系,前一步的输出是后一步的输入。(这个只是举个例子,实际的电商更复杂)
例子:用户下单流程 -> 1.校验库存 -> 2.扣减余额 -> 3.生成订单 -> 4.发送通知。
java
CompletableFuture.supplyAsync(() -> {
// 步骤1:校验库存
return inventoryService.check(stockId);
}, executor).thenCompose(isValid -> {
// 步骤2:如果校验通过,执行下一个异步任务(扣减余额)
// thenCompose 用于连接两个有依赖关系的 Future
return balanceService.deduct(userId, amount);
}, executor).thenAccept(balanceResult -> {
// 步骤3:消费结果(如记录日志或发送MQ),无返回值
log.info("扣款成功: {}", balanceResult);
}).exceptionally(ex -> {
// 统一异常处理
log.error("下单流程失败", ex);
return null;
});
多策略"竞速"与超时熔断
场景描述:为了保证高可用或低延迟,同时调用多个渠道,谁快用谁;或者防止第三方接口卡死导致系统雪崩。
例子1(竞速):同时调用"高德地图API"和"百度地图API",哪个先返回路线就用哪个。
例子2(超时):给一个耗时任务设置一个"超时闹钟"。
java
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> {
return thirdPartyApi.call(); // 模拟耗时
}, executor);
// 设置超时:如果 3秒 内没完成,则执行 fallback 逻辑
String result = task.orTimeout(3, TimeUnit.SECONDS)
.exceptionally(ex -> {
return "默认降级数据"; // 超时或异常时返回兜底数据
}).join();
耗时操作的"即返"模式(解耦)
场景描述:用户发起一个非常耗时的操作(如导出报表、生成复杂PDF、大文件上传),不能让前端一直转圈等待。
做法:Controller 层立即返回"任务已接收",后台异步执行任务,完成后通过消息通知或轮询接口告知用户。
java
// Controller 层直接返回,不等待
CompletableFuture.runAsync(() -> {
try {
exportService.exportLargeData(taskId); // 耗时操作
notificationService.sendSuccess(taskId); // 完成后通知
} catch (Exception e) {
notificationService.sendError(taskId); // 失败通知
}
}, executor);
return Result.success("任务已提交,请稍后查看进度");
制造执行系统(MES)或物联网(IoT)场景
场景描述:在工业场景中,需要实时处理设备状态、库存、订单等多个维度的并发数据。
例子:在分配生产任务时,需要同时检查"设备是否空闲"、"原材料是否充足"、"订单优先级"三个条件,全部满足才下发指令。
java
CompletableFuture.allOf(
queryDeviceStatus(deviceId),
queryMaterialStock(materialId),
queryOrderPriority(orderId)
).thenRun(() -> {
// 只有当三个条件都查询完毕后,才执行调度逻辑
if (isDeviceOk && isMaterialOk) {
schedulingService.assignTask();
}
});
生产环境避坑指南(最佳实践)
在实际业务中使用 CompletableFuture 时,请务必注意以下几点,否则容易引发线上故障:
- 必须使用自定义线程池 :
原因:CompletableFuture 默认使用 ForkJoinPool.commonPool(),其核心线程数默认等于 CPU 核数。如果你的业务是 IO 密集型(如查库、调接口),很容易耗尽线程池,导致整个系统卡死。
做法:始终使用 supplyAsync(supplier, executor) 并传入自定义的 ThreadPoolExecutor。 - 注意异常处理 :
异步线程中的异常不会自动抛到主线程,必须使用 exceptionally、handle 或 whenComplete 进行捕获和处理,否则异常会被"吞掉",导致排查问题困难。 - 避免过度嵌套 :
虽然 thenCompose 可以嵌套,但过深的嵌套(回调地狱)会降低代码可读性。如果逻辑过于复杂,建议拆分成独立的方法。 - Web 上下文丢失 :
在 Spring 中,异步线程无法直接继承主线程的 RequestAttributes 或 ThreadLocal 变量(如用户信息、TraceId)。需要在异步执行前手动传递这些上下文。