写给小白看的线程池,还有10道面试题

article/2025/9/10 8:38:09

如何搞定20k的面试小抄

为什么要用线程池呢?

下面是一段创建线程并运行的代码:

for (int i = 0; i < 100; i++) {new Thread(() -> {System.out.println("run thread->" + Thread.currentThread().getName());userService.updateUser(....);}).start();
}

我们想使用这种方式去做异步,或者提高性能,然后将某些耗时操作放入一个新线程去运行。

这种思路是没问题的,但是这段代码是存在问题的,有哪些问题呢?下面我们就来看看有哪些问题;

  • 创建销毁线程资源消耗;我们使用线程的目的本是出于效率考虑,可以为了创建这些线程却消耗了额外的时间,资源,对于线程的销毁同样需要系统资源。

  • cpu资源有限,上述代码创建线程过多,造成有的任务不能即时完成,响应时间过长。

  • 线程无法管理,无节制地创建线程对于有限的资源来说似乎成了“得不偿失”的一种作用。

既然我们上面使用手动创建线程会存在问题,那有解决方法吗?

答案:有的,使用线程池。

线程池介绍

线程池(Thread Pool):把一个或多个线程通过统一的方式进行调度和重复使用的技术,避免了因为线程过多而带来使用上的开销。

线程池有什么优点?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  • 提高线程的可管理性

线程池使用

在JDK中rt.jar包下JUC(java.util.concurrent)创建线程池有两种方式:ThreadPoolExecutor 和 Executors,其中 Executors又可以创建 6 种不同的线程池类型。

ThreadPoolExecutor 的使用

线程池使用代码如下:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class ThreadPoolDemo {private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));public static void main(String[] args) {threadPoolExecutor.execute(new Runnable() {@Overridepublic void run() {System.out.println("田先生您好");}});}
}

以上程序执行结果如下:

田先生您好

核心参数说明

ThreadPoolExecutor的构造方法有以下四个:

可以看到最后那个构造方法有 7 个构造参数,其实前面的三个构造方法只是对最后那个方法进行包装,并且前面三个构造方法最终都是调用最后那个构造方法,所以我们这里就来聊聊最后那个构造方法。

参数解释

corePoolSize

线程池中的核心线程数,默认情况下核心线程一直存活在线程池中,如果将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设为 true,如果线程池一直闲置并超过了 keepAliveTime 所指定的时间,核心线程就会被终止。

maximumPoolSize

最大线程数,当线程不够时能够创建的最大线程数。

keepAliveTime

线程池的闲置超时时间,默认情况下对非核心线程生效,如果闲置时间超过这个时间,非核心线程就会被回收。如果 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设为 true 的时候,核心线程如果超过闲置时长也会被回收。

unit

配合 keepAliveTime 使用,用来标识 keepAliveTime 的时间单位。

workQueue

线程池中的任务队列,使用 execute() 或 submit() 方法提交的任务都会存储在此队列中。

threadFactory

为线程池提供创建新线程的线程工厂。

rejectedExecutionHandler

线程池任务队列超过最大值之后的拒绝策略,RejectedExecutionHandler 是一个接口,里面只有一个 rejectedExecution 方法,可在此方法内添加任务超出最大值的事件处理。ThreadPoolExecutor 也提供了 4 种默认的拒绝策略:

  • DiscardPolicy():丢弃掉该任务,不进行处理。

  • DiscardOldestPolicy():丢弃队列里最近的一个任务,并执行当前任务。

  • AbortPolicy():直接抛出 RejectedExecutionException 异常(默认)。

  • CallerRunsPolicy():既不抛弃任务也不抛出异常,直接使用主线程来执行此任务。

包含所有参数的使用案例:

public class ThreadPoolExecutorTest {public static void main(String[] args) throws InterruptedException, ExecutionException {ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());threadPool.allowCoreThreadTimeOut(true);for (int i = 0; i < 10; i++) {threadPool.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName());try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});}}
}
class MyThreadFactory implements ThreadFactory {private AtomicInteger count = new AtomicInteger(0);@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r);String threadName = "MyThread" + count.addAndGet(1);t.setName(threadName);return t;}
}

运行输出:

main
MyThread1
main
MyThread1
MyThread1
....

这里仅仅是为了演示所有参数自定义,并没有其他用途。

execute() 和 submit()的使用

execute() 和 submit() 都是用来执行线程池的,区别在于 submit() 方法可以接收线程池执行的返回值。

下面分别来看两个方法的具体使用和区别:

// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));
// execute 使用
threadPoolExecutor.execute(new Runnable() {@Overridepublic void run() {System.out.println("老田您好");}
});
// submit 使用
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {@Overridepublic String call() throws Exception {System.out.println("田先生您好");return "返回值";}
});
System.out.println(future.get());

以上程序执行结果如下:

老田您好
田先生您好
返回值

Executors

Executors 执行器创建线程池很多基本上都是在 ThreadPoolExecutor 构造方法上进行简单的封装,特殊场景根据需要自行创建。可以把Executors理解成一个工厂类 。Executors可以创建 6 种不同的线程池类型。

下面对这六个方法进行简要的说明:

newFixedThreadPool

创建一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数。

newCacheThreadPool

短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用,如果限制 60 秒没被使用,则会被移除缓存。如果现有线程没有可用的,则创建一个新线程并添加到池中,如果有被使用完但是还没销毁的线程,就复用该线程。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

newScheduledThreadPool

创建一个数量固定的线程池,支持执行定时性或周期性任务。

newWorkStealingPool

Java 8 新增创建线程池的方法,创建时如果不设置任何参数,则以当前机器CPU 处理器数作为线程个数,此线程池会并行处理任务,不能保证执行顺序。

newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newSingleThreadScheduledExecutor

此线程池就是单线程的 newScheduledThreadPool。

线程池如何关闭?

线程池关闭,可以使用 shutdown() 或 shutdownNow() 方法,它们的区别是:

  • shutdown():不会立即终止线程池,而是要等所有任务队列中的任务都执行完后才会终止。执行完 shutdown 方法之后,线程池就不会再接受新任务了。

  • shutdownNow():执行该方法,线程池的状态立刻变成 STOP 状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,执行此方法会返回未执行的任务。

下面用代码来模拟 shutdown() 之后,给线程池添加任务,代码如下:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class ThreadPoolExecutorAllArgsTest {public static void main(String[] args) throws InterruptedException, ExecutionException {//创建线程池ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1,10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());threadPoolExecutor.allowCoreThreadTimeOut(true);//提交任务threadPoolExecutor.execute(() -> {for (int i = 0; i < 3; i++) {System.out.println("提交任务" + i);try {Thread.sleep(3000);} catch (InterruptedException e) {System.out.println(e.getMessage());}}});threadPoolExecutor.shutdown();//再次提及任务threadPoolExecutor.execute(() -> {System.out.println("我想再次提及任务");});}
}

以上程序执行结果如下:

提交任务0
提交任务1
提交任务2

可以看出,shutdown() 之后就不会再接受新的任务了,不过之前的任务会被执行完成。

面试题

面试题1:ThreadPoolExecutor 有哪些常用的方法?

ThreadPoolExecutor有如下常用方法:

  • submit()/execute():执行线程池

  • shutdown()/shutdownNow():终止线程池

  • isShutdown():判断线程是否终止

  • getActiveCount():正在运行的线程数

  • getCorePoolSize():获取核心线程数

  • getMaximumPoolSize():获取最大线程数

  • getQueue():获取线程池中的任务队列

  • allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程

这些方法可以用来终止线程池、线程池监控等。

面试题2:说说submit(和 execute两个方法有什么区别?

submit() 和 execute() 都是用来执行线程池的,只不过使用 execute() 执行线程池不能有返回方法,而使用 submit() 可以使用 Future 接收线程池执行的返回值。

说说线程池创建需要的那几个核心参数的含义

ThreadPoolExecutor 最多包含以下七个参数:

  • corePoolSize:线程池中的核心线程数

  • maximumPoolSize:线程池中最大线程数

  • keepAliveTime:闲置超时时间

  • unit:keepAliveTime 超时时间的单位(时/分/秒等)

  • workQueue:线程池中的任务队列

  • threadFactory:为线程池提供创建新线程的线程工厂

  • rejectedExecutionHandler:线程池任务队列超过最大值之后的拒绝策略

面试题3:shutdownNow() 和 shutdown() 两个方法有什么区别?

shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

面试题6:了解过线程池的工作原理吗?

当线程池中有任务需要执行时,线程池会判断如果线程数量没有超过核心数量就会新建线程池进行任务执行,如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行拒绝执行策略。

面试题5:线程池中核心线程数量大小怎么设置?

「CPU密集型任务」:比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。尽量使用较小的线程池,一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

「IO密集型任务」:比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

另外:线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程;

以上只是理论值,实际项目中建议在本地或者测试环境进行多次调优,找到相对理想的值大小。

面试题7:线程池为什么需要使用(阻塞)队列?

主要有三点:

  • 因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。

  • 创建线程池的消耗较高。

面试题8:线程池为什么要使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。

使得在线程不至于一直占用cpu资源。

(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下

 while (task != null || (task = getTask()) != null) {})。

不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?

面试题9:了解线程池状态吗?

通过获取线程池状态,可以判断线程池是否是运行状态、可否添加新的任务以及优雅地关闭线程池等。

  • RUNNING:线程池的初始化状态,可以添加待执行的任务。

  • SHUTDOWN:线程池处于待关闭状态,不接收新任务仅处理已经接收的任务。

  • STOP:线程池立即关闭,不接收新的任务,放弃缓存队列中的任务并且中断正在处理的任务。

  • TIDYING:线程池自主整理状态,调用 terminated() 方法进行线程池整理。

  • TERMINATED:线程池终止状态。

面试题10:知道线程池中线程复用原理吗?

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

总结

本文通过没有使用线程池带来的弊端,Executors介绍,Executors的六种方法介绍、如何使用线程池,了解线程池原理,核心参数,以及10到线程池面试题。

「成功不是将来才有的,而是从决定去做的那一刻起,持续累积而成。」

最近有读者想要分布式的项目,还有想要商城的,还有想要springboot,springcloud,k8s等等,这次直接分享几乎涵盖了我们java程序员的大部分技术桟,可以说真的非常全面了。强烈建议大家都上手做一做,而且以后肯定用的上。
资料包含高清视频+课件+源码……扫以下二维码并回复“1002”即可获取

点赞越多,bug越少


http://chatgpt.dhexx.cn/article/ckowoAaY.shtml

相关文章

线程池详解(通俗易懂超级好)

目标 【理解】线程池基本概念 【理解】线程池工作原理 【掌握】自定义线程池 【应用】java内置线程池 【应用】使用java内置线程池完成综合案例 线程池 线程池基础线程池使用线程池综合案例学员练习线程池总结 概念介绍 什么是线程池为什么使用线程池线程池有哪些优势 什么…

Java 多线程:彻底搞懂线程池

熟悉 Java 多线程编程的同学都知道&#xff0c;当我们线程创建过多时&#xff0c;容易引发内存溢出&#xff0c;因此我们就有必要使用线程池的技术了。 目录 1 线程池的优势 2 线程池的使用 3 线程池的工作原理 4 线程池的参数 4.1 任务队列&#xff08;workQueue&#x…

GridView概述

一、使用GridView以表格形式显示多张图片 GridView用于在界面上按行、列分布的方式来显示多个组件 二、使用GridView 1、java代码 import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.AdapterV…

Master-Detail GridView

梦幻版Master-Detail GridView(黄忠成) 2007-12-26 09:34 前面的Master-Detail GridView控件應用&#xff0c;相信你已在市面上的書、或網路上見過&#xff0c;但此節中的GridView控件應用包你沒看過&#xff0c;但一定想過&#xff01;請見圖4-8-63。 圖4-8-63 圖 4-8-64 你一…

GridView DataGrid

ASP.NET 2.0提供了功能强大的数据绑定控件GridView、在使用中&#xff0c;一些属性和方法经常会与ASP.NET 1.1中的DataGrid混淆(VS2005中依然可以使用DataGrid&#xff0c;手动添加到工具箱或HTML状态输入代码)&#xff0c;下面我们分别用GridView和DataGrid实现其数据绑定、编…

GridView详讲

GridView是ASP.NET界面开发中的一个重要的控件&#xff0c;对GridView使用的熟练程度直接影响软件开发的进度及功能的实现。(车延禄) GridView的主要新特性&#xff1a; 1.与DataSource控件结合实现了显示与数据操作的分离&#xff0c;大大减化了代码的编写量; 2.实现"双向…

GridView详述

GridView无代码分页排序GridView选中&#xff0c;编辑&#xff0c;取消&#xff0c;删除GridView正反双向排序GridView和下拉菜单DropDownList结合GridView和CheckBox结合鼠标移到GridView某一行时改变该行的背景色方法一鼠标移到GridView某一行时改变该行的背景色方法二GridVi…

GridView、ListView、Adapter、Map、HashMap

1.ListView自定义适配器adapter 注&#xff1a;Android适配器是数据和视图之间的桥梁&#xff0c;以便于数据在View上显示。适配器就像显示器&#xff0c;把复杂的东西按人可以接受的方式来展现。 &#xff08;1&#xff09;首先将适配器的View视图表现出来&#xff0c;使用L…

GridViewPager

GridViewPager ViewPager结合GridView&#xff0c;轻松实现类似表情面板的控件。可自由定制Item布局&#xff0c;提供充足的自定义参数等。也处理了条目点击事件和条目长按事件。效果如下&#xff1a; Demo下载地址&#xff1a;GridViewPager &#xff0c;或者扫描以下二维码…

libevent 编译

1.下载源码 github:https://github.com/libevent/libevent 官网&#xff1a;http://libevent.org/ 2.CMake 编译 在libevent源码目录建立文件夹&#xff1a;BuildVs2010_x64 2.打开CMake 3.BuildVs2010_x64 下此时生成了vs2010的解决方案。然后编译生成就ok NOTE&#x…

13、《Libevent中文帮助文档》学习笔记13:Linux下集成、运行libevent

Linux下编译libevent的指导可以参考《4、《Libevent中文帮助文档》学习笔记4&#xff1a;Linux下编译libevent》&#xff0c;完成编译、安装&#xff0c;生成so库后&#xff0c;其他程序即可依赖libevent的so库&#xff0c;使用libevent的功能。 由于没有通过prefix指定安装路…

libevent 编译与安装 (WIN10 visual studio2019, ubuntu,centos)

文章目录 一、准备安装包二、编译与安装编译zlib编译openssl编译libevent 三、libevent集成zlib测试程序修改编译&#xff08;可选&#xff09;四、测试程序五、linux(ubuntu)测试安装依赖环境&#xff0c;依次编译zlib,openssl,libeventwindows与linux共享文件夹&#xff08;使…

Libevent 学习一:Libevent 源码编译

文章目录 Libevent 学习一&#xff1a;Libevent 源码编译Libevent Windows 编译Windows 编译环境安装 Visual Studio Community 2015安装 zlib安装 OpenSSL安装 Libeventcmake 安装 LibeventLibevent 测试程序 Libevent Linux编译CentOS 7 安装 LibeventLibevent 测试程序 Libe…

libevent mysql_libevent安装总结

1.先用&#xff1a;ls -al /usr/lib | grep libevent 查看是否已安装&#xff1b;如果已安装且版本低于1.3&#xff0c;则先通过&#xff1a;rpm -e libevent —nodeps进行卸载。 2.下载libevent安装包&#xff1a;libevent-2.0.18-stable.tar.gz。 wget https://github.com/do…

在window用vcpkg安装libevent

参考readme https://github.com/microsoft/vcpkg/blob/master/README_zh_CN.md 使用的PackageManager方式安装&#xff0c; Package Managers 下载 vcpkg 依赖管理包 git clone https://github.com/Microsoft/vcpkg.git cd vcpkg ./bootstrap-vcpkg.bat ./vcpkg integrate…

libevent实践01:准备源码、搭建项目、编译脚本和入门例子

编译源码 libevent是一个Reactor事件库。 我的理解&#xff0c;就是封装了select、epoll、poll的函数库。有使用select&#xff0c;poll&#xff0c;epoll的需求就可以使用的。 官网地址&#xff1a;https://libevent.org/ 下载源码&#xff1a; https://github.com/libev…

libevent(1)windows下安装libevent

Socket通信库libevent成熟、稳定、性能高&#xff0c;在unix和windows下都能使用&#xff0c;在证券交易领域也有不少成功的应用&#xff0c;已经用事实证明是非常棒的socket通信库。对我们目前交易系统的unix重构来说&#xff0c;是比较合适的选择 –– 坑少、在证券交易项目中…

Libevent库的学习

目录 Libevent 概述 Libevent 使用模型 使用Libevent的基本流程&#xff1a; libevent 的核心&#xff0c;event 事件 1. 创建一个事件event 2. 释放event_free 3. 注册event 4. 信号事件 5. 销毁event_base Libevent 结构图 使用libevent库去实现tcp服务器 Libev…

编译libevent

本文记录在win10编译libevent的过程 1.编译前准备zlib,openssl zlib网址 http://www.zlib.net/ 下载源码解压缩 打开vs的dos窗口 32位选择32位窗口这里选择64位,cd 到解压后的文件夹 执行 nmake /f win32/Makefile.msc 执行后可以看到目录下有lib文件和dll文件和测试文件…

libevent简要介绍

libevent库 开源。精简。跨平台&#xff08;Windows、Linux、Maxos、unix&#xff09;。专注于网络通信。 源码包安装&#xff1a; ./configure 检查安装环境 生成makefile make 生成.o和可执行文件 sudo make install …