一、进程和线程
1. 进程(Process)
-
定义:进程是操作系统资源分配的基本单位,是程序的一次执行过程。每个进程都有独立的内存空间、文件描述符、环境变量等。
-
特点:
-
独立性:进程之间相互隔离,一个进程崩溃不会影响其他进程。
-
资源开销大:创建和切换进程需要较大的系统开销。
-
通信复杂:进程间通信(IPC)需要通过管道、消息队列、共享内存等方式。
-
2. 线程(Thread)
-
定义:线程是CPU调度的基本单位,是进程中的一个执行流。一个进程可以包含多个线程,所有线程共享进程的内存空间和资源。
-
特点:
-
轻量级:创建和切换线程的开销比进程小。
-
共享资源:线程可以直接访问进程的全局变量和堆内存。
-
通信简单:线程间通信可以通过共享内存实现,但需要同步机制(如锁)来避免竞争。
-
3.进程与线程的区别
特性 | 进程 | 线程 |
---|---|---|
定义 | 操作系统资源分配的基本单位 | CPU调度的基本单位,进程中的执行流 |
内存空间 | 独立的内存空间 | 共享进程的内存空间 |
资源开销 | 创建和切换开销大 | 创建和切换开销小 |
通信方式 | 进程间通信(IPC)复杂(如管道、消息队列) | 线程间通信简单(共享内存) |
独立性 | 进程间相互隔离,一个进程崩溃不影响其他进程 | 线程共享资源,一个线程崩溃可能影响整个进程 |
并发性 | 进程间并发 | 线程间并发 |
创建与销毁 | 创建和销毁速度慢 | 创建和销毁速度快 |
适用场景 | 需要高隔离性的任务(如浏览器多标签页) | 需要高并发、低开销的任务(如Web服务器) |
总结
-
进程是资源分配的基本单位,适合需要高隔离性的任务。
-
线程是CPU调度的基本单位,适合需要高并发、低开销的任务。
-
理解进程和线程的区别是掌握多线程编程和操作系统原理的基础。
二、线程生命周期
Java线程的生命周期分为6种状态(根据Thread.State
枚举):
-
NEW(新建):线程被创建但未调用
start()
。 -
RUNNABLE(可运行):调用
start()
后,线程在JVM中等待CPU调度或正在运行。 -
BLOCKED(阻塞):等待获取监视器锁(如进入
synchronized
代码块)。 -
WAITING(无限期等待):调用
Object.wait()
、Thread.join()
或LockSupport.park()
,需其他线程唤醒。 -
TIMED_WAITING(限期等待):带超时的等待,如
Thread.sleep(ms)
、Object.wait(timeout)
。 -
TERMINATED(终止):线程执行完毕或异常退出。
状态转换示例:
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000); // 进入TIMED_WAITING
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start(); // 进入RUNNABLE
面试重点:
-
BLOCKED
与WAITING
的区别?
BLOCKED
是等待锁,WAITING
是主动调用等待方法。
三、线程池原理(ThreadPoolExecutor)
“线程池”是一种用于管理和复用线程的技术,它通过预先创建一组线程并维护一个任务队列,来高效地处理并发任务。线程池的核心思想是避免频繁创建和销毁线程,从而减少系统开销,提高性能。
线程池的核心概念
1. 为什么需要线程池?
-
线程创建和销毁开销大:频繁创建和销毁线程会消耗大量系统资源。
-
资源管理复杂:手动管理线程容易导致资源耗尽或线程泄漏。
-
任务调度优化:线程池可以合理分配任务,避免线程过多导致上下文切换频繁。
2. 线程池的组成部分
-
核心线程(Core Threads):线程池中始终存活的线程,即使空闲也不会被销毁。
-
任务队列(Work Queue):用于存放待执行的任务。
-
非核心线程(Non-Core Threads):当任务队列满时,线程池会创建额外的线程来处理任务。
-
拒绝策略(Rejected Execution Handler):当线程池和任务队列都满时,如何处理新提交的任务。
线程池的工作原理
-
提交任务:当有新任务提交时,线程池会优先使用核心线程处理。
-
任务队列:如果核心线程都在忙,任务会被放入任务队列等待。
-
创建非核心线程:如果任务队列已满,线程池会创建非核心线程处理任务。
-
拒绝策略:如果线程池和任务队列都满,新任务会触发拒绝策略。
Java中的线程池实现
Java通过java.util.concurrent
包提供了线程池的实现,核心类是ThreadPoolExecutor
。
1. 创建线程池
ExecutorService executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, TimeUnit.SECONDS, // 非核心线程空闲存活时间
new LinkedBlockingQueue<>(10), // 任务队列
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
2. 核心参数:
-
corePoolSize(核心线程数):即使空闲也不会被回收的线程数。
-
maximumPoolSize(最大线程数):线程池允许的最大线程数。
-
keepAliveTime(空闲线程存活时间):非核心线程空闲超过此时间会被回收。
-
workQueue(任务队列):用于保存待执行任务的阻塞队列(如
LinkedBlockingQueue
)。 -
RejectedExecutionHandler(拒绝策略):当线程池和队列都满时的处理策略。
3. 拒绝策略
-
AbortPolicy(默认):抛出
RejectedExecutionException
。 -
CallerRunsPolicy:由提交任务的线程直接执行。
-
DiscardPolicy:静默丢弃新任务。
-
DiscardOldestPolicy:丢弃队列中最旧的任务,重试提交。
线程池的使用场景
-
Web服务器:处理大量并发请求。
-
数据处理:批量处理数据任务。
-
定时任务:通过
ScheduledThreadPoolExecutor
执行定时任务。
线程池的优点
-
降低资源消耗:复用线程,减少创建和销毁的开销。
-
提高响应速度:任务到达时可以直接执行,无需等待线程创建。
-
提高线程的可管理性:统一管理线程的生命周期和任务分配。
面试常见问题
-
线程池的核心参数有哪些?
核心线程数、最大线程数、任务队列、拒绝策略、非核心线程存活时间。 -
如何选择合适的线程池参数?
根据任务类型(CPU密集型、IO密集型)和系统资源调整。 -
线程池的拒绝策略有哪些?
AbortPolicy
、CallerRunsPolicy
、DiscardPolicy
、DiscardOldestPolicy
。 -
线程池的任务队列满了会怎样?
如果任务队列满且线程数达到最大值,会触发拒绝策略。
总结
线程池是并发编程中的重要工具,通过合理配置线程池参数和拒绝策略,可以显著提高系统的性能和稳定性。理解线程池的工作原理和使用场景,是掌握多线程编程的关键。