JDK17/JDK21并发编程小白入门:资深架构常用模式+最佳实践(通俗易懂版)
先跟小白说句大实话:并发编程不是"炫技",是大厂开发、高并发系统(比如电商秒杀、APP接口)的"必备技能",也是资深架构和普通程序员的核心差距之一。
很多小白一听到"并发""多线程"就头大,觉得全是复杂理论、看不懂也用不上------其实不然。这篇文章全程不用难懂的术语,不搞空洞理论,只讲 JDK17、JDK21里最常用、最实用的并发知识,梳理高企资深架构天天在用的经典模式、最佳实践,还有高频踩坑点,小白式拆解,保证你看完能懂、上手能用。
先明确一个核心前提:JDK17是"稳定款"(企业生产环境首选),JDK21是"升级款"(主打并发性能优化,新项目/高并发场景首选),两者的并发特性是"兼容且升级"的,我们重点讲"两者都能用、资深架构必用"的内容,区分开"JDK21专属优化",避免你学混。
一、小白必懂:并发编程到底是什么?(一句话讲透)
不用背定义,举2个生活例子,瞬间明白:
-
单线程:你一个人做饭,先洗菜→切菜→炒菜→盛饭,一步做完才能做下一步,效率低;
-
并发:你(洗菜)、你对象(切菜)、你妈(炒菜)一起动手,同时做不同的事,最后一起完成,效率翻倍。
对应到编程里:"单线程"就是程序一次只做一件事,"并发"就是程序同时做多件事(比如一个接口同时被1000个人调用,程序要同时处理这1000个请求)。
而JDK17、JDK21做的事,就是给你"提供现成的工具",让你不用自己从零写"多个人一起干活"的逻辑,还能避免"多人干活乱套"(比如两个人同时炒一盘菜,最后炒糊),这就是我们要学的"并发编程工具+模式"。
补充一句:高企里的"高并发场景"(比如秒杀、直播带货),本质就是"很多人同时找程序干活",学好并发,就是让程序"能扛住、不卡顿、不出错"。
二、核心基础:JDK17/JDK21并发编程的"3个核心工具"(必懂,拿来就用)
资深架构写并发代码,从来不会"从零造轮子",全是用JDK自带的工具。这3个工具,是所有并发场景的"基石",JDK17和JDK21都能用,小白必须先吃透、记牢。
2.1 工具1:线程(Thread)------ 并发的"干活的人"
还是用做饭的例子:线程就是"干活的人",一个线程=一个干活的人。
重点讲小白能懂的核心点,复杂的底层跳过:
-
JDK17及之前:我们用的是"普通线程"(也叫OS线程)------ 就像"正式员工",每个员工(线程)要占一个工位(内存),工位成本高(每个线程默认占1MB内存),公司(JVM)最多能雇几千个"正式员工",多了就没工位(OOM报错);
-
JDK21专属优化:新增"虚拟线程"------ 就像"临时工",不用固定工位(内存占用只有几十KB,还能动态调整),公司(JVM)能雇百万级、千万级的"临时工",成本极低。
小白重点记:什么时候用普通线程,什么时候用虚拟线程?(资深架构的选型逻辑,直接抄)
-
用普通线程(JDK17/JDK21都能用):干活时间长、不闲着(比如计算100万条数据、复杂算法)------ 类似"正式员工干核心重活";
-
用虚拟线程(仅JDK21能用):干活时间短、经常等(比如调用其他接口、查数据库、读文件)------ 类似"临时工干杂活,等的时候可以去干别的",比如1000个人同时查订单,线程大部分时间在等数据库响应,用虚拟线程性价比极高。
举个小白能看懂的代码例子(极简,不用纠结细节,看思路):
java
// JDK17 普通线程(正式员工)
Thread thread = new Thread(() -> {
// 这里写要干的活,比如计算数据
System.out.println("正式员工干活:计算数据");
});
thread.start(); // 让员工开始干活
// JDK21 虚拟线程(临时工,写法更简单)
Thread.startVirtualThread(() -> {
// 这里写要干的活,比如查数据库
System.out.println("临时工干活:查数据库");
});
2.2 工具2:锁(Lock)------ 避免"干活乱套"的规则
还是做饭例子:如果两个人同时炒一盘菜(同一个资源),必然会炒糊、乱套。锁,就是"规定谁先炒、谁后炒"的规则,保证同一时间,只有一个人(线程)能碰这盘菜(资源)。
小白重点记:JDK17/JDK21里,资深架构最常用的2种锁,不用学其他的,吃透这2种就够了:
(1)ReentrantLock(重入锁)------ 灵活的"手动锁"(JDK17/JDK21都能用)
就像"厨房的门钥匙",谁拿到钥匙,谁就能进厨房炒菜,其他人只能等。特点是"灵活",能自己控制"什么时候拿钥匙、什么时候还钥匙",适合复杂场景。
资深架构的最佳实践(小白直接抄代码,不用改):
java
// 1. 先创建一把锁(相当于厨房钥匙)
Lock lock = new ReentrantLock();
// 2. 干活的时候,先拿锁,干完还锁(避免别人一直等)
try {
lock.lock(); // 拿钥匙(进入厨房)
// 这里写要干的活,比如操作同一个变量、修改同一条数据库数据
System.out.println("拿到锁,开始干活,别人不能碰");
} finally {
lock.unlock(); // 还钥匙(离开厨房),必须写在finally里,防止忘还钥匙(死锁)
}
小白避坑点(高频踩坑,必看):
-
千万别忘写unlock()!如果不写,拿到钥匙的线程崩溃了,钥匙就永远不还了,其他人永远进不去厨房(死锁),程序直接卡住;
-
unlock()必须写在finally里,不管干活成功还是失败,都能保证钥匙归还。
(2)synchronized(同步锁)------ 简单的"自动锁"(JDK17/JDK21都能用)
就像"厨房的自动门",有人进去(线程干活),门就自动锁上,其他人等;人出来(线程干完),门自动解锁,不用手动控制。特点是"简单",适合简单场景,不用写复杂的拿锁、还锁逻辑。
资深架构的最佳实践(小白直接抄):
java
// 方式1:加在方法上(整个方法都是"厨房",进去就锁门)
public synchronized void cook() {
// 干活逻辑,比如炒一盘菜
System.out.println("自动锁:开始炒菜,别人不能进");
}
// 方式2:加在代码块上(只有代码块是"厨房",更灵活)
public void cook() {
// 其他不用锁的逻辑(比如准备盘子)
synchronized (this) {
// 要锁的逻辑(比如炒菜)
System.out.println("自动锁:开始炒菜,别人不能进");
}
}
小白选型技巧(资深架构常用):
-
简单场景(比如单个方法、简单变量操作):用synchronized,不用写多余代码,不容易出错;
-
复杂场景(比如需要超时重试、中断等待、多条件判断):用ReentrantLock,更灵活。
2.3 工具3:线程池(ThreadPool)------ 高效"管理干活的人"(JDK17/JDK21都能用)
还是做饭例子:如果每次做饭,都要重新找一个人(创建线程)、教他怎么干(初始化线程),干完再让他走(销毁线程),效率极低。
线程池,就是"固定的团队"------ 公司(JVM)提前雇好一批人(线程),有活了直接分配,干完活不用走,等着下一个活,不用每次都创建、销毁,效率翻倍。
小白重点记:资深架构从来不会"每次用线程都新建一个",全是用线程池,因为创建/销毁线程的成本太高,高并发场景下会拖垮程序。
JDK17/JDK21最常用的线程池(小白直接抄代码,不用自己配置):
java
// 1. 创建线程池(固定10个"员工",适合大部分普通场景)
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 2. 给线程池分配活(提交任务)
for (int i = 0; i < 100; i++) {
threadPool.submit(() -> {
// 要干的活,比如处理一个用户请求、查一次数据库
System.out.println("线程池里的员工干活中");
});
}
// 3. 程序结束时,关闭线程池(不然程序不会停)
threadPool.shutdown(); // 优雅关闭:等所有活干完再关闭
// threadPool.shutdownNow(); // 强制关闭:不管活有没有干完,立即停止(慎用)
资深架构的核心配置技巧(小白不用死记,理解即可):
-
线程池大小(多少个员工):普通IO场景(查数据库、调接口),设置为 CPU核心数 * 2;CPU密集场景(计算数据),设置为 CPU核心数 + 1;
-
不用自己手动创建线程池(比如new ThreadPoolExecutor),用Executors提供的现成方法(newFixedThreadPool、newCachedThreadPool),简单且不易出错;
-
JDK21优化:线程池支持直接提交"虚拟线程",不用手动创建虚拟线程,写法更简单(小白了解即可,后续讲模式时再详细说)。
三、重点中的重点:高企资深架构常用的"5个并发经典模式"(JDK17/JDK21实战版)
这部分是核心中的核心------ 资深架构写并发代码,全是套用这些模式,不用自己瞎琢磨,小白学会这些,就能应对80%的企业并发场景(秒杀、接口高并发、数据批量处理等)。
所有模式都结合JDK17/JDK21特性,用小白能懂的语言拆解,附上极简实战代码(直接抄),还有最佳实践。
模式1:单例模式(并发安全版)------ 全局只有一个"工具"(JDK17/JDK21必用)
场景举例:程序里的"日志工具""数据库连接工具",我们希望整个程序只有一个实例(比如只有一个日志工具,不然日志会乱套),这就是单例模式。
小白重点:单例模式必须保证"并发安全"------ 不能多个线程同时创建出多个实例(比如两个线程同时创建日志工具,最后有两个日志工具,日志混乱)。
资深架构最常用的写法(JDK17/JDK21通用,并发安全,直接抄):
java
// 单例模式:全局只有一个实例
public class LogUtil {
// 1. 私有构造方法(禁止别人手动创建实例)
private LogUtil() {}
// 2. 静态内部类(懒加载,只有用到的时候才创建实例)
private static class LogUtilHolder {
// 唯一实例
private static final LogUtil INSTANCE = new LogUtil();
}
// 3. 提供公共方法,获取实例(并发安全,JVM自动保证)
public static LogUtil getInstance() {
return LogUtilHolder.INSTANCE;
}
// 业务方法:写日志
public void log(String message) {
System.out.println(message);
}
}
最佳实践(小白必记):
-
不用学"饿汉式""双重检查锁",就用这种"静态内部类"写法------ 简单、并发安全、懒加载(不用的时候不占内存),资深架构首选;
-
JDK17可以用"密封类(sealed)"修饰LogUtil,禁止别人继承,更安全(前文讲过密封类,这里直接套用)。
模式2:生产者-消费者模式 ------ 解耦"干活的人"(JDK17/JDK21必用)
场景举例:电商秒杀中,"用户下单"是生产者(产生订单任务),"订单处理"是消费者(处理订单任务);或者"日志收集"是生产者(产生日志),"日志写入文件"是消费者(处理日志)。
核心价值:让生产者和消费者"互不干扰"------ 生产者只管产生任务,消费者只管处理任务,中间用一个"任务队列"衔接,就算生产者产生任务太快,消费者也能慢慢处理,不会拖垮生产者(比如秒杀时,1000个人同时下单,订单处理不过来,就先存到队列里,慢慢处理)。
资深架构实战写法(JDK17/JDK21通用,直接抄):
java
public class ProducerConsumerDemo {
// 1. 任务队列(衔接生产者和消费者,存订单任务)
private static final BlockingQueue<String> taskQueue = new ArrayBlockingQueue<>(100);
public static void main(String[] args) {
// 2. 生产者线程池(3个生产者,负责产生订单)
ExecutorService producerPool = Executors.newFixedThreadPool(3);
// 3. 消费者线程池(5个消费者,负责处理订单)
ExecutorService consumerPool = Executors.newFixedThreadPool(5);
// 生产者产生任务(模拟100个用户下单)
for (int i = 0; i < 100; i++) {
int orderId = i;
producerPool.submit(() -> {
try {
String task = "订单" + orderId;
taskQueue.put(task); // 把订单放到队列里
System.out.println("生产者:产生" + task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 消费者处理任务
for (int i = 0; i < 5; i++) {
consumerPool.submit(() -> {
while (true) {
try {
String task = taskQueue.take(); // 从队列里拿订单,没有就等
System.out.println("消费者:处理" + task);
// 模拟处理订单的耗时(比如查库存、扣库存)
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
}
// 关闭线程池(实际开发中,程序结束时关闭)
producerPool.shutdown();
consumerPool.shutdown();
}
}
最佳实践(小白必记):
-
任务队列用BlockingQueue(JDK自带),不用自己写队列------ 它自带"阻塞"功能,没有任务时,消费者会自动等(不用手动写等待逻辑),任务满了,生产者会自动等,避免出错;
-
生产者和消费者用线程池,不用单个线程------ 高并发场景下,单个线程扛不住;
-
JDK21优化:可以把消费者线程换成"虚拟线程",处理更多任务,写法更简单(把consumerPool换成虚拟线程池即可)。
模式3:Future模式 ------ 异步"拿结果"(JDK17/JDK21必用)
场景举例:你去餐厅吃饭,点完菜(提交任务),不用一直等菜上桌(不用一直阻塞线程),可以玩手机(线程干别的活),菜做好了,服务员会叫你(拿到任务结果)------ 这就是Future模式。
核心价值:避免"线程阻塞",提高效率------ 比如一个接口需要同时查"用户信息""订单信息""优惠券信息",不用等查完用户信息,再查订单信息,而是同时查,最后汇总结果,接口响应速度翻倍。
资深架构实战写法(JDK17/JDK21通用,直接抄):
java
public class FutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 2. 异步查用户信息(提交任务,不用等结果,直接返回Future)
Future<String> userFuture = threadPool.submit(() -> {
Thread.sleep(500); // 模拟查数据库耗时
return "用户信息:张三,ID:1001";
});
// 3. 异步查订单信息(同时执行,不用等用户信息查完)
Future<String> orderFuture = threadPool.submit(() -> {
Thread.sleep(300); // 模拟查数据库耗时
return "订单信息:订单ID:2001,金额:100元";
});
// 4. 干别的活(比如查优惠券信息),不用等上面两个结果
System.out.println("干别的活:查优惠券信息");
Thread.sleep(200);
// 5. 拿到异步任务的结果(如果没做好,会等做好再拿)
String userInfo = userFuture.get(); // 拿到用户信息
String orderInfo = orderFuture.get(); // 拿到订单信息
// 6. 汇总结果
System.out.println("汇总结果:" + userInfo + "," + orderInfo);
// 关闭线程池
threadPool.shutdown();
}
}
最佳实践(小白必记):
-
用Future.get()拿结果时,会"阻塞线程"------ 所以一定要先干别的活,最后再拿结果,不然就失去了异步的意义;
-
如果不想一直等,可以用Future.get(1, TimeUnit.SECONDS)------ 设置超时时间(比如1秒),超过1秒没拿到结果,就抛出异常,避免线程一直阻塞;
-
JDK21优化:新增FutureTask的简化写法,不用手动提交线程池,直接用虚拟线程执行,代码更简洁(小白了解即可,重点掌握上面的通用写法)。
模式4:结构化并发模式 ------ 管理"一组相关任务"(JDK21专属,资深架构新宠)
场景举例:一个接口需要"查用户信息+查订单信息+查优惠券信息",这三个任务是"相关的"------ 只要有一个任务失败(比如查用户信息失败),另外两个任务就不用执行了,还要一起取消,避免浪费资源。
核心价值:解决"多任务混乱"的问题------ 之前用Future模式,多个任务之间互不关联,一个失败,其他还在执行,浪费资源;结构化并发,能把一组相关任务"绑在一起",统一管理、统一取消、统一处理结果。
资深架构实战写法(仅JDK21能用,直接抄):
java
public class StructuredConcurrentDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建结构化任务作用域(绑定一组相关任务)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 2. 提交三个相关任务(查用户、查订单、查优惠券)
Future<String> userFuture = scope.fork(() -> {
Thread.sleep(500);
// 模拟任务失败:如果用户ID错误,抛出异常
// throw new RuntimeException("查用户信息失败");
return "用户信息:张三";
});
Future<String> orderFuture = scope.fork(() -> {
Thread.sleep(300);
return "订单信息:100元";
});
Future<String> couponFuture = scope.fork(() -> {
Thread.sleep(200);
return "优惠券信息:10元";
});
// 3. 等待所有任务完成(如果有一个失败,其他任务会被自动取消)
scope.join();
// 4. 拿到所有任务结果
String userInfo = userFuture.resultNow();
String orderInfo = orderFuture.resultNow();
String couponInfo = couponFuture.resultNow();
System.out.println("汇总结果:" + userInfo + "," + orderInfo + "," + couponInfo);
}
// 作用域自动关闭,所有任务会被自动清理,不用手动关闭线程池
}
}
最佳实践(小白必记):
-
结构化并发是JDK21专属,JDK17不能用------ 新项目、高并发接口,优先用这种模式,比Future模式更简洁、更安全;
-
核心是"ShutdownOnFailure()"------ 意思是"一个任务失败,所有任务都取消",适合"多个任务必须同时成功"的场景(比如下单时,查库存、扣库存、减优惠券,必须都成功,有一个失败就取消);
-
不用手动创建线程池、关闭线程池,作用域会自动管理,减少出错概率。
模式5:虚拟线程池模式 ------ 百万级并发的"秘密武器"(JDK21专属,高并发首选)
场景举例:电商秒杀、APP首页接口,同时有10万、100万用户请求,用普通线程池,最多只能处理几千个请求,会直接拖垮程序;用虚拟线程池,能轻松处理百万级请求,且内存占用极低。
核心价值:JDK21的"杀手锏"------ 不用手动创建虚拟线程,直接用虚拟线程池,写法和普通线程池几乎一样,就能实现"百万级并发",不用改太多代码。
资深架构实战写法(仅JDK21能用,直接抄,秒杀场景首选):
java
public class VirtualThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建虚拟线程池(JDK21新增,写法和普通线程池几乎一样)
ExecutorService virtualThreadPool = Executors.newVirtualThreadPerTaskExecutor();
// 2. 提交100万个任务(模拟百万级用户请求)
for (int i = 0; i < 1000000; i++) {
int requestId = i;
virtualThreadPool.submit(() -> {
// 模拟接口处理逻辑(查数据库、调下游接口)
try {
Thread.sleep(10); // 模拟耗时
System.out.println("处理请求:" + requestId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 3. 关闭虚拟线程池
virtualThreadPool.shutdown();
}
}
最佳实践(小白必记):
-
虚拟线程池只能用在JDK21,且只适合"IO密集场景"(查数据库、调接口、读文件),CPU密集场景用普通线程池,不然性能会下降;
-
创建虚拟线程池,只用Executors.newVirtualThreadPerTaskExecutor(),不用自己配置,简单且高效;
-
百万级并发场景(秒杀、直播),优先用虚拟线程池,不用考虑内存溢出------ 虚拟线程内存占用极低,100万个虚拟线程,内存占用只有几十MB。
四、高频核心原理(小白能懂版,不用背,理解即可)
资深架构面试时,经常会问这些原理,但不用背复杂术语,理解下面的"小白版解释",就能应对面试,也能更好地用对并发工具。
4.1 并发安全的核心原理:可见性、原子性、有序性(小白版)
-
可见性:一个线程修改了数据,其他线程能立即看到------ 比如你修改了订单状态,其他线程能立即看到修改后的状态,不然会出现"一个线程以为订单没支付,另一个线程以为已经支付"的错误;
-
原子性:一个操作要么全部完成,要么全部不完成,不能中途打断------ 比如扣库存,"库存从100减到99",要么完成,要么不完成,不能出现"库存变成99.5"或者"扣了库存但没保存"的情况;
-
有序性:程序执行的顺序,和我们写的顺序一致------ 比如你写了"先查库存,再扣库存",程序不能先扣库存再查库存,不然会出现超卖。
小白重点:我们前面学的锁(synchronized、ReentrantLock)、线程池、并发模式,本质都是为了"保证这三个特性",避免并发安全问题------ 不用深究底层,只要知道"用对这些工具,就能保证并发安全"即可。
4.2 死锁的核心原理(小白版,避坑关键)
死锁:两个线程"互相等对方的钥匙",谁也不让谁,最后都卡住,程序崩溃------ 比如线程A拿着钥匙1,等着线程B的钥匙2;线程B拿着钥匙2,等着线程A的钥匙1,两人一直等,永远干不了活。
小白避坑:只要记住"三个避免",就不会出现死锁:
-
避免一个线程拿多把锁;
-
避免锁的顺序混乱(比如线程A先拿钥匙1再拿钥匙2,线程B也必须先拿钥匙1再拿钥匙2);
-
避免忘记归还锁(unlock()写在finally里)。
4.3 JDK21并发优化的核心原理(小白版)
JDK21的并发优化,本质就是"降低成本、提高效率",不用背底层,记住3个核心优化即可:
-
虚拟线程:降低线程的内存成本,从"每个线程1MB"降到"每个线程几十KB",支持百万级并发;
-
结构化并发:统一管理相关任务,避免资源浪费,简化多任务并发的写法;
-
虚拟线程池:不用手动创建虚拟线程,直接用线程池的写法,就能实现百万级并发,降低学习和使用成本。
五、实用注意事项(小白必看,高频踩坑点汇总)
这部分是资深架构多年踩坑总结的"经验之谈",小白看完,能避免90%的并发编程错误,少走很多弯路。
5.1 通用注意事项(JDK17/JDK21都适用)
-
永远不要手动创建线程(new Thread()),全用线程池------ 手动创建/销毁线程成本太高,高并发场景下会拖垮程序;
-
锁的范围越小越好------ 比如只锁"炒菜"的逻辑,不锁"准备盘子"的逻辑,提高并发效率;
-
避免在锁里面做"耗时操作"(比如查数据库、调接口)------ 不然其他线程会一直等,并发效率极低;
-
线程池一定要关闭------ 不用的时候,必须调用shutdown(),不然程序不会正常停止;
-
不要用ThreadLocal存储"共享数据"------ ThreadLocal是"每个线程单独存储数据",共享数据存在里面,其他线程看不到,会出现数据不一致。
5.2 JDK17专属注意事项
-
JDK17没有虚拟线程,高并发IO场景,用普通线程池,线程池大小设置为CPU核心数*2;
-
用synchronized锁方法时,不要用"静态方法锁"(synchronized static)------ 会锁整个类,并发效率极低;
-
单例模式优先用"静态内部类"写法,不用双重检查锁(容易出错)。
5.3 JDK21专属注意事项
-
虚拟线程只适合"IO密集场景",CPU密集场景(计算数据),还是用普通线程池,不然性能会下降;
-
结构化并发的作用域,必须用try-with-resources写法(try (var scope = ...)),不然会出现资源泄漏;
-
虚拟线程池(newVirtualThreadPerTaskExecutor()),不用手动设置线程池大小,JVM会自动管理;
-
JDK21的并发特性,和JDK17兼容------ 之前写的JDK17并发代码,升级到JDK21后,不用修改就能直接运行。
六、最后总结(小白必看,划重点)
-
并发编程不用怕,核心就是"用对JDK自带的工具"------ 线程(普通+虚拟)、锁(synchronized+ReentrantLock)、线程池,再套用5个经典模式,就能应对80%的企业场景;
-
JDK17和JDK21的选择:老项目、稳定优先,用JDK17;新项目、高并发IO场景(秒杀、接口),用JDK21,优先用虚拟线程和结构化并发;
-
小白学习顺序:先吃透3个核心工具 → 再套用5个经典模式 → 最后记住注意事项,避坑即可,不用深究复杂底层;
-
资深架构的核心思路:"不重复造轮子、优先用JDK原生工具、简化代码、保证并发安全"------ 我们学的,就是这套思路。
最后提醒:并发编程的核心是"实践",把上面的代码抄下来,运行一遍,修改一下参数(比如线程池大小、任务数量),看看效果,慢慢就懂了,比背理论有用10倍。