1. 从“单打独斗”到“协同作战”:Java并行执行的核心价值
在Java的世界里,我们写的代码默认是“单线程”的,就像一个人从头到尾处理所有事情。这在处理简单任务时没问题,但一旦遇到需要同时处理多个任务,比如一个电商网站需要同时响应成千上万个用户的请求,或者一个数据分析程序需要处理海量文件,这种“单打独斗”的模式就会成为性能瓶颈,导致响应缓慢、资源闲置。这时,我们就需要引入“并行执行”的能力,让程序能够“协同作战”,同时处理多个任务,从而大幅提升吞吐量和响应速度。
Java作为一门成熟的企业级语言,其并行执行的能力也在不断演进。从最经典的 线程 ,到后来为了应对复杂异步场景而兴起的 异步编程 ,再到近年来旨在简化并发编程、提升资源利用率的 虚拟线程 ,Java为我们提供了多种武器库。理解这三种方式,不仅仅是应付面试中的“Java八股文”,更是我们构建高性能、高并发应用的核心技能。很多朋友在面试中被问到“线程创建的三种方式”时对答如流,但在实际项目中,面对一个具体的性能瓶颈,却不知道是该开个新线程,还是用 CompletableFuture ,或者干脆重构为虚拟线程。这篇文章,我将结合自己十多年的踩坑经验,为你彻底拆解这三种并行执行方式的核心原理、适用场景和实战中的那些“坑”,让你不仅知其然,更知其所以然,在面对具体问题时能做出最合适的技术选型。
2. 线程:并行世界的基石与双刃剑
线程是操作系统能够进行运算调度的最小单位,也是Java实现并行最基础、最直接的方式。一个Java进程可以包含多个线程,它们共享进程的内存空间(堆、方法区),但各自拥有独立的程序计数器、虚拟机栈和本地方法栈。这种共享内存的模型使得线程间通信非常高效,但也带来了同步和资源竞争的复杂性。
2.1 线程的创建与管理:从基础到演进
Java中创建线程主要有三种传统方式,这也是面试中的经典问题。
第一种,继承 Thread 类。 这是最直观的方式,但也是限制最多的一种。你创建一个类继承 Thread ,并重写 run 方法。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行: " + Thread.currentThread().getName());
}
}
// 使用
new MyThread().start();
这种方式的问题在于,Java是单继承的,一旦你的类继承了 Thread ,就无法再继承其他类,这在设计上缺乏灵活性。在实际项目中,除非是非常简单的场景,否则我几乎不会使用这种方式。
第二种,实现 Runnable 接口。 这是更推荐的方式。它解耦了任务( Runnable )和执行线程( Thread )本身。
public class MyTask implements Runnable {
@Override
public void run() {
System.out.println("任务执行: " + Thread.currentThread().getName());
}
}
// 使用
Thread thread = new Thread(new MyTask());
thread.start();
这种方式的好处是,你的任务类可以自由地继承其他类或实现更多接口,符合面向接口编程的原则。 Runnable 接口本身是一个函数式接口(只有一个抽象方法 run ),这为后来Lambda表达式的使用埋下了伏笔。
第三种,实现 Callable 接口并结合 Future 。 当你的线程需要返回一个结果,或者可能抛出受检异常时, Runnable 就无能为力了。 Callable 接口的 call 方法可以返回泛型结果并抛出异常。
public class ComputeTask implements Callable<Integer> {
private int a;
private int b;
public ComputeTask(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public Integer call() throws Exception {
// 模拟耗时计算
Thread.sleep(1000);
return a + b;
}
}
// 使用,需要借助ExecutorService
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new ComputeTask(1, 2));
Integer result = future.get(); // 阻塞等待结果
executor.shutdown();
Future 对象就像一个“提货单”,你提交任务后立刻拿到这个单子,可以继续做别的事情,在需要结果的时候再凭单子去取(调用 get 方法)。如果结果还没计算好, get 方法会阻塞当前线程。
注意: 直接使用
new Thread().start()的方式在简单Demo中没问题,但在生产环境是灾难性的。因为创建和销毁一个操作系统线程的成本非常高(涉及系统调用和内存分配),无限制地创建线程会导致系统资源(如内存、CPU时间片)被迅速耗尽,最终可能抛出OutOfMemoryError或导致系统僵死。这就是为什么我们需要线程池。
2.2 线程池:管理线程的生命周期
线程池( ThreadPoolExecutor )是管理线程生命周期的核心工具。它预先创建好一定数量的线程放在“池”中,当有任务提交时,从池中分配一个空闲线程来执行,执行完毕后线程不销毁,而是返回池中等待下一个任务。这避免了频繁创建销毁线程的开销。
Java通过 Executors 工厂类提供了一些快捷的线程池创建方法,但了解其背后的 ThreadPoolExecutor 构造参数至关重要:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // 核心线程数,即使空闲也会保留
maximumPoolSize, // 最大线程数
keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
workQueue, // 任务队列
threadFactory, // 线程工厂,用于定制线程属性(如名称、优先级)
handler // 拒绝策略,当队列和线程池都满时如何处理新任务
);
核心参数解析与避坑:
-
corePoolSize与maximumPoolSize: 不要盲目设置。对于CPU密集型任务(如计算圆周率),线程数接近CPU核心数通常最佳,过多会导致频繁的上下文切换。对于IO密集型任务(如网络请求、数据库查询),线程数可以设置得多一些,因为线程在等待IO时不会占用CPU。一个经验公式:线程数 = CPU核心数 * (1 + 平均等待时间 / 平均计算时间)。 -
workQueue(任务队列): 常用的有LinkedBlockingQueue(无界队列)和ArrayBlockingQueue(有界队列)。使用无界队列时,maximumPoolSize </


3171

被折叠的 条评论
为什么被折叠?



