多线程面试一
目录
进程和线程的区别
线程的创建方式有几种及区别
线程池类结构
线程池参数
线程池拒绝策略
线程池工作流程
进程和线程的区别
定义: 进程: 进程是操作系统进行资源分配和调度的基本单位。每个进程都有自己的独立内存空间,并且拥有独立的地址空间、数据段、堆栈段等。
线程: 线程是进程中的一个执行单元,是调度和执行的基本单位。同一进程中的多个线程共享进程的资源,包括内存空间、文件句柄等。
资源分配: 进程拥有独立的资源,比如内存空间、文件句柄等。 线程则共享进程的资源,除了CPU寄存器和栈之外,其他资源都是共享的。
上下文切换: 进程间的上下文切换涉及更多的系统资源,因此开销较大。 线程间的上下文切换只涉及少量寄存器和栈的保存与恢复,开销较小。
通信机制: 进程间通信(IPC)通常需要通过操作系统提供的机制,如管道、消息队列、信号量等。 线程间可以直接访问共享内存,通信更加直接和高效。
创建和销毁: 创建一个新的进程涉及加载新的地址空间,分配资源等,因此创建进程的开销较大。 创建线程的开销相对较小,因为不需要额外的地址空间。
并发执行: 在同一时刻,一个进程可以有多个线程并发执行。 线程的并发执行可以通过CPU的时间片轮转机制来实现。
隔离性: 进程之间是相互隔离的,每个进程有自己的地址空间,因此一个进程的崩溃不会影响到另一个进程。 线程在同一进程中共享资源,因此一个线程的错误可能会导致整个进程崩溃。
线程的创建方式有几种及区别
继承 Thread 类: 定义: 定义一个类继承 Thread 类,并重写 run() 方法。 优点: 简单易用,可以直接访问和设置线程属性。 缺点: 不能继承其他类,因为Java不支持多重继承。
实现 Runnable 接口: 定义: 定义一个类实现 Runnable 接口,并实现 run() 方法。 优点: 可以同时实现其他接口,灵活性较高。 缺点: 需要通过 Thread 构造器传入 Runnable 实例。
实现 Callable 接口: 定义: 定义一个类实现 Callable 接口,并实现 call() 方法。 优点: 支持返回值和抛出异常。 缺点: 需要使用 FutureTask 包装 Callable 对象。
使用线程池: 定义: 利用 ExecutorService 接口提供的线程池来管理线程。 优点: 可以复用线程减少创建和销毁线程的开销,适用于大量线程的情况。 缺点: 需要额外配置线程池。
使用 ThreadFactory: 定义: ThreadFactory 提供了一种创建线程的方法,通常与线程池一起使用。 优点: 可以通过 ThreadFactoryBuilder (Guava库) 或自定义实现来创建具有特定属性的线程。 缺点: 需要额外配置。
回答完总结 继承 Thread 类 和 实现 Runnable 接口 是最常用的创建线程的方式,适用于大多数情况。 实现 Callable 接口 适用于需要线程返回结果的场景。 使用线程池 适用于需要管理大量线程的应用,可以有效控制线程数量并复用线程。 使用 ThreadFactory 适用于需要定制线程属性的情况,通常与线程池一起使用。
线程池类结构
Executor 这是所有线程池的顶层接口,它仅定义了一个方法execute(Runnable command),用于执行给定的Runnable任务。但是,Executor本身并不提供线程池的功能,它只是一个简单的执行器。
ExecutorService 继承自Executor,ExecutorService提供了更丰富的线程管理和控制功能,包括启动、停止线程池,以及控制任务的执行。它定义了一些额外的方法,如submit()、invokeAll()、invokeAny()和shutdown()等。
AbstractExecutorService这是一个抽象类,实现了ExecutorService接口,提供了线程池的一些基础实现,比如shutdown()和isShutdown()方法。子类通常只需要实现特定的方法,如execute()。
ThreadPoolExecutor 这是线程池的核心实现类,继承自AbstractExecutorService。ThreadPoolExecutor提供了对线程池的详细控制,包括线程数量、队列类型、线程工厂、拒绝策略等。它是创建自定义线程池的主要类。
ScheduledExecutorService 这个接口也继承自ExecutorService,但它专注于定时任务的执行,允许以固定延迟或周期性地执行任务。
ScheduledThreadPoolExecutor 这是ScheduledExecutorService的一个实现,基于ThreadPoolExecutor,它支持定时和周期性的任务执行。
线程池参数
创建线程池时,通常需要指定以下参数:
corePoolSize : 核心线程数,即线程池中的线程数目达到此数量后,新来的任务会被放入任务队列中等待执行。
maximumPoolSize: 最大线程数,当任务队列满时,线程池会继续创建新的线程来处理任务,直到达到这个最大值。
keepAliveTime: 当线程池中的线程数目超过核心线程数时,多余的空闲线程的存活时间。如果设置为0,则多余的线程会立即终止。
workQueue: 用于存放等待执行的任务的阻塞队列,常见的有ArrayBlockingQueue, LinkedBlockingQueue等。
threadFactory: 创建新线程的工厂,可以自定义线程的属性,如优先级、守护状态等。
handler: 拒绝策略,当任务队列和线程池都已满时,如何处理新来的任务。
线程池拒绝策略
线程池在无法接受更多任务时,会根据拒绝策略来处理新任务:
AbortPolicy: 抛出RejectedExecutionException异常,默认策略。
CallerRunsPolicy: 调用者所在的线程会执行该任务,可能会降低调用者的响应速度或吞吐量。
DiscardPolicy: 直接丢弃任务,不做任何处理也不抛出异常。
DiscardOldestPolicy: 丢弃队列里最近的一个任务,并尝试再次提交当前任务。
线程池工作流程
提交任务: 用户通过submit()或execute()方法向线程池提交任务。
分配任务: 如果当前运行的线程少于corePoolSize,即使线程池中有其他空闲的线程,也会创建一个新的线程来处理任务。
排队: 如果当前运行的线程等于corePoolSize,那么任务会被放入workQueue中等待执行。
扩展线程: 如果workQueue满了且当前线程数小于maximumPoolSize,则创建新的线程来处理任务。
拒绝任务: 如果当前线程数等于maximumPoolSize且workQueue也满了,那么根据handler策略处理任务。
回收线程: 当线程池中的线程数目大于corePoolSize时,超出的线程会在空闲keepAliveTime时间后被终止。