文章目录
0. 前言
程序员面试本是一件再平常不过的事情,记得刚毕业的时候面试题背的滚瓜烂熟。但是在职程序员面试却是另一回事了,我们往往没有太多时间复习,特别是大龄程序员,工作日忙于工作,周末还要照顾家庭,一旦面临被优化的风险就很被动,难以在短时间内复习并找到工作。不要问我是怎么知道的,都是切身体会,在复习的过程中我也走了不少弯路,所幸最终结果令自己满意。
为了不让和我一样的程序员遇到同样的问题,我打算写这一系列的文章,这些文章不会像其他面经一般大而全,这些文章仅记录我在复习过程中认为重要的知识点,如果能帮助到你就太好了。
1. 线程的定义、和进程的区别、线程的生命周期
一句话回答:线程是进程中独立运行的最小单位,共享进程的资源,而进程是操作系统分配资源的最小单位,拥有独立的内存空间。
细节解释:
1.1 定义
线程是进程中独立运行的最小单位,它拥有自己的堆栈、程序计数器和局部变量,但与进程共享相同的内存空间、打开的文件和全局变量。
1.2 进程和线程的区别
- 资源分配:进程是操作系统分配资源的最小单位,拥有独立的内存空间、打开的文件和全局变量。线程则是进程中独立运行的最小单位,共享进程的资源。
- 执行方式:进程可以独立执行,而线程必须依附于进程才能执行。
- 创建开销:创建进程的开销比创建线程的开销大。
- 通信方式:进程间通信需要使用IPC(进程间通信)机制,而线程间通信可以直接访问共享内存。
1.3 线程的生命周期
- 新建 (New):线程被创建但尚未启动。
- 就绪 (Runnable):线程已准备就绪,等待 CPU 资源。
- 运行 (Running):线程正在执行。
- 阻塞 (Blocked):线程被阻塞,等待某个事件发生,例如 I/O 操作完成或锁释放。
- 死亡 (Dead):线程执行完毕或遇到异常而终止。
线程是进程中更轻量级的执行单元,它们共享进程的资源,但拥有独立的执行路径。理解线程和进程的区别以及线程的生命周期,对于编写高效、可靠的多线程程序至关重要。
2. 在Java中创建线程有哪些方式
一句话回答:Java中创建线程的方式主要有三种:继承Thread类、实现Runnable接口、使用Callable和Future。
细节解释:
2.1 继承Thread类
- 创建一个继承自
Thread
的类,并重写run
方法。 - 示例代码:
class MyThread extends Thread { public void run() { System.out.println("MyThread is running."); } } // 使用 MyThread myThread = new MyThread(); myThread.start();
2.2 实现Runnable接口
- 创建一个实现
Runnable
接口的类,实现run
方法。 - 示例代码:
class MyRunnable implements Runnable { public void run() { System.out.println("MyRunnable is running."); } } // 使用 Thread thread = new Thread(new MyRunnable()); thread.start();
2.3 使用Callable和Future
- 创建一个实现
Callable
接口的类,实现call
方法,并返回值。 - 使用
FutureTask
包装Callable任务。 - 示例代码:
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; class MyCallable implements Callable<Integer> { public Integer call() { System.out.println("MyCallable is running."); return 123; } } // 使用 FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); // 获取结果 try { Integer result = futureTask.get(); // 阻塞直到任务完成 System.out.println("Result: " + result); } catch (Exception e) { e.printStackTrace(); }
2.4 使用Executor框架
- 使用
Executors
创建线程池,并使用ExecutorService
提交任务。 - 示例代码:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; // 创建固定大小的线程池 ExecutorService executorService = Executors.newFixedThreadPool(2); // 提交任务 executorService.submit(new MyRunnable()); // 关闭线程池 executorService.shutdown();
2.5 ThreadPoolExecutor
- 使用
ThreadPoolExecutor
创建线程池提供了高度的自定义性,允许开发者根据应用需求精确配置线程池的行为。 - 示例代码:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 线程工厂,用于创建具有名称前缀的线程,方便识别
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("custom-pool-%d").build();
// 饱和策略,定义当任务太多来不及处理时的行为
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
// 创建线程池
int corePoolSize = 5; // 核心线程数
int maximumPoolSize = 10; // 最大线程数
long keepAliveTime = 1; // 非核心线程空闲存活时间
TimeUnit unit = TimeUnit.MINUTES; // 存活时间单位
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // 工作队列
ExecutorService executorService = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 提交任务到线程池
for (int i = 0; i < 10; i++) {
int finalI = i;
executorService.submit(() -> {
System.out.println("执行任务: " + finalI + " 由线程 " + Thread.currentThread().getName() + " 处理");
});
}
// 关闭线程池,不再接受新任务,已提交的任务将执行完毕
executorService.shutdown();
try {
// 等待线程池关闭,即所有任务执行完毕
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
// 超时后强制关闭线程池
executorService.shutdownNow();
}
} catch (InterruptedException e) {
// 当前线程被中断时的处理
executorService.shutdownNow();
}
}
}
3. 为什么阿里不建议使用Executors类的静态方法创建线程
一句话回答:阿里巴巴推荐使用ThreadPoolExecutor
而非Executors
类创建线程池,是因为前者提供了更精细的控制和自定义配置,有助于防止资源耗尽并增强线程池的健壮性。
细节解释:
- 细粒度控制:
ThreadPoolExecutor
允许开发者设置核心线程数、最大线程数、存活时间、工作队列等参数,实现对线程池行为的精确控制。 - 自定义线程工厂:通过实现
ThreadFactory
接口,可以创建具有统一命名的线程,便于问题追踪和日志记录。 - 参数合理配置:合理配置线程池参数,平衡资源使用和系统性能,避免资源浪费或系统过载。
- 灵活的拒绝策略:
RejectedExecutionHandler
提供多种任务拒绝策略,适应不同应用场景,增强线程池的健壮性。 - 避免资源耗尽:
Executors
类可能因缺乏有效资源限制和拒绝策略,在高并发下导致资源过度消耗。 - 线程池生命周期管理:
ExecutorService
接口提供灵活的线程池生命周期管理,包括任务提交和关闭操作。 - 资源合理释放:通过
shutdown
或shutdownNow
方法,确保线程池在不再需要时及时释放资源,防止内存泄漏。 - 避免使用Executors的缺点:
Executors
类可能使用无界队列或不适当默认参数,高负载下可能导致线程数量失控。 - 提高线程池的可维护性:自定义配置和细粒度控制使得
ThreadPoolExecutor
创建的线程池更易于监控和维护,提升应用程序稳定性。
4. 线程池的核心参数有哪些
一句话回答:线程池的核心参数包括线程池初始大小、最大线程数、空闲线程存活时间、时间单位、工作队列和拒绝策略,它们共同决定了线程池如何有效管理和执行任务。
细节解释:
- 线程池初始大小(corePoolSize):线程池中始终保持的线程数量,即使线程空闲也不会被回收。
- 最大线程数(maximumPoolSize):线程池中允许的最大线程数量,超过这个数量的新任务将被放入工作队列或根据拒绝策略处理。
- 空闲线程存活时间(keepAliveTime):设置当线程数超过初始大小时,多余的空闲线程在被终止前等待新任务的最长时间。
- 时间单位(unit):与空闲线程存活时间配合使用,定义了存活时间的时间单位,如秒、毫秒等。
- 工作队列(workQueue):用于存放待处理任务的阻塞队列,当线程池中的线程全忙时,新提交的任务将入队等待。
- 拒绝策略(handler):当工作队列满了且线程池中的线程数达到最大线程数时,线程池将根据设定的拒绝策略来处理新提交的任务。
Java线程池的默认拒绝策略是AbortPolicy,它会抛出一个RejectedExecutionException异常,同时还有其他几种预定义的拒绝策略可供选择。
- AbortPolicy:默认策略,当任务被拒绝时会抛出
RejectedExecutionException
异常,并且会打印一条日志信息。 - CallerRunsPolicy:这个策略将任务的执行权交给调用
execute(Runnable)
方法的线程,即提交任务的线程会尝试自己运行这个任务。 - DiscardPolicy:这个策略会直接丢弃被拒绝的任务,不抛出异常,也不执行任务。
- DiscardOldestPolicy:这个策略会丢弃工作队列中等待时间最长的任务,然后尝试重新提交当前任务。
除了这些预定义的拒绝策略外,开发者还可以通过实现RejectedExecutionHandler
接口来定义自己的拒绝策略,以便更灵活地处理任务拒绝的情况。自定义拒绝策略可以包括记录日志、发送通知、动态增加线程资源等操作。
5. 线程池设置多大合适
一句话回答:线程池大小的选择策略应基于任务特性、系统资源和并发需求,并通过实际测试调整以找到最优解。
细节解释:
- 任务特性:CPU密集型任务的线程池大小应接近处理器核心数,而I/O密集型任务可以适当增加线程数以提高资源利用率。
- 系统资源:考虑服务器的CPU核心数和内存限制,避免线程过多导致资源竞争和上下文切换。
- 并发需求:根据系统的最大并发任务数预估线程池大小,确保在高并发情况下系统能够稳定响应。
- 经验公式:常用的经验公式为
Nthreads = Ncpu * (Ucpu + W/C)
,其中Ncpu
是CPU核心数,Ucpu
是CPU使用率,W
是任务等待时间,C
是任务计算时间。 - 性能测试:通过实际的性能测试来评估不同线程池大小对系统性能的影响,并据此调整大小。
- 自适应调整:在某些场景下,可以采用动态调整策略,根据实时监控数据自动调整线程池大小。
6. CompletableFuture是什么
一句话回答:CompletableFuture
是 Java 8 中引入的用于异步编程的类,它提供了一种编写非阻塞代码的方式,允许以声明式的风格处理异步操作。
细节解释:
CompletableFuture
支持以同步或异步的方式表示一个未来某个时刻完成的操作,并且可以对其进行链式调用,实现复杂的异步逻辑。
代码示例:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "完成";
});
future.thenAccept(result -> {
System.out.println(result); // 处理结果
}).join(); // 等待异步操作完成
// 可以链式添加更多的操作
future.thenApply(s -> s.toUpperCase())
.thenAccept(upperCaseResult -> System.out.println(upperCaseResult));
在这个例子中,supplyAsync
方法异步地执行了一个耗时的操作,并返回了一个 CompletableFuture
对象。thenAccept
方法添加了一个回调,用于在异步操作完成时处理结果。join
方法用于等待异步操作完成。链式调用 thenApply
对结果进行了进一步的处理,并且 thenAccept
用于输出处理后的结果。