查看原文
其他

等不及了,冲字节去了!

小林coding 小林coding 2024-04-19

图解学习网站:https://xiaolincoding.com

大家好,我是小林。

很多同学都开工了,先祝大家开工大吉!

春节期间,图解网站挂了一次,原因是又欠费了,之前 CDN 流量包只买了国内和亚太流量包,忘记买北美流量包,导致一直在扣费,停机了。

看来很多北美留学生春节都在图解网站上学习,赶紧充了一波钱,不能耽误大家学习,现在网站都已经恢复正常了。

春节结束, 24 届的同学可以开展春招了,25 届的同学也可以开始开展春招实习了。

不知道同学们春节期间有复习和准备吗?

在这里,小林建议准备到 70 分的同学,就可以去投了,不要等着 100 分才去投,永远不可能准备完美的,面试也是需要多面才能越来越有感觉。

而且越早投优势越大,卷的人相对没有那么多,我认识一些二本进大厂实习的同学,都是在春招实习刚开始的第一个月就马上投了,拖到越后面,学历好的同学也慢慢准备好面试了,竞争的人就会越多了。

每面完一次,失败也没关系,不要因为一两次的失败就否定的自己努力,重在复盘和加强遗漏没学到的内容。

今天给大家分享一位 Java 后端开发同学的字节校招面经,在准备春招的同学,也可以看看自己准备的知识是否能覆盖真实大厂面试的知识点,如果感觉能答出6-7 层,恭喜你, 可以开始投起来了!

考察的知识点:

  • MySQL:日志、varchar、优化方式
  • Redis:应用场景、 zset数据结构、跳表、压缩列表、定期删除
  • 网络协议:键入网址、状态码、nginx 负载均衡算法
  • 操作系统:进程间通信方式、linux 命令
  • Java:ThreadLocal、线程池

MySQL

MySQL 有哪些日志?

  • 慢查询日志:记录执行时间超过阈值的SQL查询语句。用于优化查询性能,找出慢查询并进行优化。
  • redo log:用于InnoDB存储引擎的崩溃恢复。
  • binlog:记录所有对数据的更改操作的日志。它包含了对数据库进行插入、更新和删除操作的详细信息,以二进制形式记录,可以用于数据的备份和恢复、主从复制等场景
  • Undo Log:实现事务的原子性,记录事务对数据的修改操作,方便在事务失败或回滚时恢复数据到事务前的状态,确保数据的一致性。支持事务的回滚和MVCC(多版本并发控制)机制。

重启恢复读取的是哪个文件?

崩溃恢复是读 redo log文件。

redo log 是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志。

在事务提交时,通过WAL机制,先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。

当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。

redo log 的作用是:

  • 实现事务的持久性,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
  • 将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。

如果redolog日志文件太大怎么办?

默认情况下, InnoDB 存储引擎有 1 个重做日志文件组( redo log Group),「重做日志文件组」由有 2 个 redo log 文件组成,这两个 redo 日志的文件名叫 :ib_logfile0ib_logfile1

重做日志文件组

在重做日志组中,每个 redo log File 的大小是固定且一致的,假设每个 redo log File 设置的上限是 1 GB,那么总共就可以记录 2GB 的操作。

重做日志文件组是以循环写的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形。

所以 InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件。

重做日志文件组写入过程

我们知道 redo log 是为了防止 Buffer Pool 中的脏页丢失而设计的,那么如果随着系统运行,Buffer Pool 的脏页刷新到了磁盘中,那么 redo log 对应的记录也就没用了,这时候我们擦除这些旧记录,以腾出空间记录新的更新操作。

redo log 是循环写的方式,相当于一个环形,InnoDB 用 write pos 表示 redo log 当前记录写到的位置,用 checkpoint 表示当前要擦除的位置,如下图:

图中的:

  • write pos 和 checkpoint 的移动都是顺时针方向;
  • write pos ~ checkpoint 之间的部分(图中的红色部分),用来记录新的更新操作;
  • check point ~ write pos 之间的部分(图中蓝色部分):待落盘的脏数据页记录;

如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL 恢复正常运行,继续执行新的更新操作。

所以,一次 checkpoint 的过程就是脏页刷新到磁盘中变成干净页,然后标记 redo log 哪些记录可以被覆盖的过程。

char和varchar的区别?

Char和Varchar是两种用于存储文本数据的数据类型,它们的主要区别在于存储方式和使用场景:

  • Char是一种固定长度的数据类型,它始终占用指定长度的存储空间,不论实际存储的数据长度是多少。如果存储的文本长度固定不变,可以使用Char。
  • Varchar是一种可变长度的数据类型,它只占用实际存储数据长度所需的存储空间,可以节省存储空间。如果存储的文本长度变化较大,可以使用Varchar。

MySQL有哪些优化方式?

  • 针对慢SQL可以进SQL优化,优化的方式可以建立联合索引进行索引覆盖优化,减少回表;对排序字段增加索引避免 file sort的问题;使用小表驱动大;or改成union;大偏移量的limit,先通过>id找到第一页的 id,再limit。
  • 针对热点数据可以构建缓存,查询的时候先查缓存,减少对 mysql 的访问提高查询效率。
  • 读请求过大的时候,可以构建 mysql 主从架构,进行读写分离,由多个从机来承接读请求的流量。
  • 写请求过大的时候,可以进行分库,由多个 mysql 机器来承接写请求的流量。
  • 客户端可以增加客户端连接池,减少客户端与 mysql 连接的建立和释放的开销,复用连接。

Redis

Redis为什么快?

官方使用基准测试的结果是,单线程的 Redis 吞吐量可以达到 10W/每秒,如下图所示:

之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:

  • Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
  • Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
  • Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

你项目中有哪些地方用到了Redis?

用zset 实现了热点排行榜。

Zset的数据结构是什么?

Zset 类型的底层数据结构是由压缩列表或跳表实现的:

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

讲一下跳表吧?

链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快读定位数据。

那跳表长什么样呢?我这里举个例子,下图展示了一个层级为 3 的跳表。

图中头节点有 L0~L2 三个头指针,分别指向了不同层级的节点,然后每个层级的节点都通过指针连接起来:

  • L0 层级共有 5 个节点,分别是节点1、2、3、4、5;
  • L1 层级共有 3 个节点,分别是节点 2、3、5;
  • L2 层级只有 1 个节点,也就是节点 3 。

如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到节点 4,因为可以在头节点直接从 L2 层级跳到节点 3,然后再往前遍历找到节点 4。

可以看到,这个查找过程就是在多个层级上跳来跳去,最后定位到元素。当数据量很大时,跳表的查找复杂度就是 O(logN)。

那跳表节点是怎么实现多层级的呢?这就需要看「跳表节点」的数据结构了,如下:

typedef struct zskiplistNode {
    //Zset 对象的元素值
    sds ele;
    //元素权重值
    double score;
    //后向指针
    struct zskiplistNode *backward;
  
    //节点的level数组,保存每层上的前向指针和跨度
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

Zset 对象要同时保存「元素」和「元素的权重」,对应到跳表节点结构里就是 sds 类型的 ele 变量和 double 类型的 score 变量。每个跳表节点都有一个后向指针(struct zskiplistNode *backward),指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。

跳表是一个带有层级关系的链表,而且每一层级可以包含多个节点,每一个节点通过指针连接起来,实现这一特性就是靠跳表节点结构体中的zskiplistLevel 结构体类型的 level 数组

level 数组中的每一个元素代表跳表的一层,也就是由 zskiplistLevel 结构体表示,比如 leve[0] 就表示第一层,leve[1] 就表示第二层。zskiplistLevel 结构体里定义了「指向下一个跳表节点的指针」和「跨度」,跨度时用来记录两个节点之间的距离。

比如,下面这张图,展示了各个节点的跨度。

第一眼看到跨度的时候,以为是遍历操作有关,实际上并没有任何关系,遍历操作只需要用前向指针(struct zskiplistNode *forward)就可以完成了。

跨度实际上是为了计算这个节点在跳表中的排位。具体怎么做的呢?因为跳表中的节点都是按序排列的,那么计算某个节点排位的时候,从头节点点到该结点的查询路径上,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的排位。

为什么层数增加的时候是0.25?

跳表的相邻两层的节点数量的比例会影响跳表的查询性能。

举个例子,下图的跳表,第二层的节点数量只有 1 个,而第一层的节点数量有 6 个。

这时,如果想要查询节点 6,那基本就跟链表的查询复杂度一样,就需要在第一层的节点中依次顺序查找,复杂度就是 O(N) 了。所以,为了降低查询复杂度,我们就需要维持相邻层结点数间的关系。

**跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)**。

下图的跳表就是,相邻两层的节点数量的比例是 2 : 1。

如果采用新增节点或者删除节点时,来调整跳表节点以维持比例的方法的话,会带来额外的开销。

Redis 则采用一种巧妙的方法是,跳表在创建节点的时候,随机生成每个节点的层数,并没有严格维持相邻两层的节点数量比例为 2 : 1 的情况。

具体的做法是,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数

这样的做法,相当于每增加一层的概率不超过 25%,层数越高,概率越低,层高最大限制是 64。

讲一下压缩列表吧?

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。

img

压缩列表在表头有三个字段:

  • zlbytes,记录整个压缩列表占用对内存字节数;
  • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
  • zllen,记录压缩列表包含的节点数量;
  • zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段(zllen)的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了,因此压缩列表不适合保存过多的元素

另外,压缩列表节点(entry)的构成如下:

img

压缩列表节点包含三部分内容:

  • prevlen,记录了「前一个节点」的长度,目的是为了实现从后向前遍历;
  • encoding,记录了当前节点实际数据的「类型和长度」,类型主要有两种:字符串和整数。
  • data,记录了当前节点的实际数据,类型和长度都由 encoding 决定;

当我们往压缩列表中插入数据时,压缩列表就会根据数据类型是字符串还是整数,以及数据的大小,会使用不同空间大小的 prevlen 和 encoding 这两个元素里保存的信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是 Redis 为了节省内存而采用的

讲一下定期删除底层源码是怎么实现的吧?

定期删除策略的做法:每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。

1、这个间隔检查的时间是多长呢?

在 Redis 中,默认每秒进行 10 次过期检查一次数据库,此配置可通过 Redis 的配置文件 redis.conf 进行配置,配置键为 hz 它的默认值是 hz 10。

特别强调下,每次检查数据库并不是遍历过期字典中的所有 key,而是从数据库中随机抽取一定数量的 key 进行过期检查。

2、随机抽查的数量是多少呢?

我查了下源码,定期删除的实现在 expire.c 文件下的 activeExpireCycle 函数中,其中随机抽查的数量由 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 定义的,它是写死在代码中的,数值是 20。

也就是说,数据库每轮抽查时,会随机选择 20 个 key 判断是否过期。

接下来,详细说说 Redis 的定期删除的流程:

  1. 从过期字典中随机抽取 20 个 key;
  2. 检查这 20 个 key 是否过期,并删除已过期的 key;
  3. 如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。

可以看到,定期删除是一个循环的流程。

那 Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。

定期删除的流程,我写了个伪代码:

do {
    //已过期的数量
    expired = 0
    //随机抽取的数量
    num = 20;
    while (num--) {
        //1. 从过期字典中随机抽取 1 个 key
        //2. 判断该 key 是否过期,如果已过期则进行删除,同时对 expired++
    }
    
    // 超过时间限制则退出
    if (timelimit_exit) return;

  /* 如果本轮检查的已过期 key 的数量,超过 25%,则继续随机抽查,否则退出本轮检查 */
while (expired > 20/4); 

定期删除的流程如下:

网络

讲一下一条http请求输入域名之后的过程?

输入URL过程如下:

图片
  • DNS 解析:当用户输入一个网址并按下回车键的时候,浏览器获得一个域名,而在实际通信过程中,我们需要的是一个 IP 地址,因此我们需要先把域名转换成相应 IP 地址。
  • TCP 连接:浏览器通过 DNS 获取到 Web 服务器真正的 IP 地址后,便向 Web 服务器发起 TCP 连接请求,通过 TCP 三次握手建立好连接。
  • 建立TCP协议时,需要发送数据,发送数据在网络层使用IP协议, 通过IP协议将IP地址封装为IP数据报;然后此时会用到ARP协议,主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址,找到目的MAC地址;
  • IP数据包在路由器之间,路由选择使用OPSF协议, 采用Dijkstra算法来计算最短路径树,抵达服务端。
  • 发送 HTTP 请求:建立 TCP 连接之后,浏览器向 Web 服务器发起一个 HTTP 请求(如果是HTTPS协议,发送HTTP 请求之前还需要完成TLS四次握手);
  • 处理请求并返回:服务器获取到客户端的 HTTP 请求后,会根据 HTTP 请求中的内容来决定如何获取相应的文件,并将文件发送给浏览器。
  • 浏览器渲染:浏览器根据响应开始显示页面,首先解析 HTML 文件构建 DOM 树,然后解析 CSS 文件构建渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上。

状态码502,504有什么区别?

502状态码表示无效的响应,而504状态码表示等待响应超时,两者都与网关或代理服务器有关。

  • 状态码502表示“Bad Gateway”,通常指代网关或代理服务器从上游服务器接收到无效的响应。这意味着代理服务器无法从上游服务器获取有效的响应。

  • 状态码504表示“Gateway Timeout”,表示在网关或代理服务器尝试访问上游服务器时,等待服务器响应超时。这通常发生在代理服务器等候上游服务器响应的时间超过了限制。

Nginx有哪些负载均衡算法?

Nginx支持的负载均衡算法包括:

  • 轮询:按照顺序依次将请求分配给后端服务器。这种算法最简单,但是也无法处理某个节点变慢或者客户端操作有连续性的情况。

  • IP哈希:根据客户端IP地址的哈希值来确定分配请求的后端服务器。适用于需要保持同一客户端的请求始终发送到同一台后端服务器的场景,如会话保持。

  • URL哈希:按访问的URL的哈希结果来分配请求,使每个URL定向到一台后端服务器,可以进一步提高后端缓存服务器的效率。

  • 最短响应时间:按照后端服务器的响应时间来分配请求,响应时间短的优先分配。适用于后端服务器性能不均的场景,能够将请求发送到响应时间快的服务器,实现负载均衡。

  • 加权轮询:按照权重分配请求给后端服务器,权重越高的服务器获得更多的请求。适用于后端服务器性能不同的场景,可以根据服务器权重分配请求,提高高性能服务器的利用率。

Nginx位于七层网络结构中的哪一层?

应用层,nginx 是七层负载均衡。

操作系统

进程间的通信方式?

进程间的通信方式包括:

  • 管道:管道是一种半双工的通信方式,用于具有亲缘关系的进程间通信,通常是父子进程或兄弟进程之间使用。数据只能单向流动,且具有一定的容量限制。应用场景包括父子进程之间的通信或者在Shell脚本中使用。

  • 命名管道:命名管道也是一种半双工的通信方式,但可以在无亲缘关系的进程间进行通信。命名管道在文件系统中有对应的文件名,不受进程关系限制。应用场景包括进程间的数据交换、进程间通信等。

  • 信号:信号是一种异步的通信方式,用于通知接收进程发生了某种事件。常见的信号有SIGKILL、SIGTERM等。应用场景包括进程终止通知、进程状态变化通知等。

  • 消息队列:消息队列是一种通过消息实现进程间通信的方式,允许多个进程向同一个队列发送消息或接收消息。消息队列可以实现进程之间的异步通信,提供了一种高效的通信方式。应用场景包括进程解耦、进程间数据交换等。

  • 共享内存:共享内存是一种高效的进程间通信方式,多个进程可以访问同一块内存区域。可以实现进程间数据共享,但需要考虑同步和互斥问题。应用场景包括大数据量的数据交换、共享缓存等。

  • 信号量:信号量是一种用于进程间同步和互斥的机制,可以控制多个进程对共享资源的访问。通过信号量可以实现进程间的同步和互斥操作。应用场景包括进程同步、资源互斥访问等。

  • Socket通信:Socket通信是一种全双工的通信方式,通过网络套接字(socket)在不同主机上的进程进行通信。可以实现客户端与服务器之间的通信,包括TCP和UDP两种协议。应用场景包括网络编程、客户端与服务器之间的数据传输、实时通讯等。

讲一下你用过哪些Linux的命令?

用过 top、netstat、grep、sed、awk 这些命令

sed和awk有什么区别?

都是用于文本处理命令的工具,区别在于:

  • sed,主要用于对文本进行替换、删除、插入等操作。它适合对整行文本进行处理,可以通过正则表达式匹配文本进行操作。
  • awk,可以实现更复杂的文本处理逻辑,包括对字段的操作、条件判断、循环等。它适合处理结构化的文本数据,可以按列对数据进行处理。

sed适合简单的文本替换和编辑操作,而awk适合处理结构化的文本数据并实现更复杂的处理逻辑

Java

用过ThreadLocal吗?

用过。

ThreadLocal用于创建线程局部变量。每个 ThreadLocal 对象都可以维护一个线程本地变量,可以使线程间的数据隔离,以此来解决多线程同时访问共享变量的安全性。

ThreadLocal是在哪个包下的?

ThreadLocal类位于java.lang包下,是JDK提供的一个类。

讲一下ThreadLocal的get和set是怎么实现的?

ThreadLocal 的 get 方法

我们看下 get 方法,源码如下:

Copy/**
 * 返回当前 ThreadLocal 对象关联的值
 *
 * @return
 */

public T get() {
 // 返回当前 ThreadLocal 所在的线程
 Thread t = Thread.currentThread();
 // 从线程中拿到 ThreadLocalMap
 ThreadLocalMap map = getMap(t);
 if (map != null) {
  // 从 map 中拿到 entry
  ThreadLocalMap.Entry e = map.getEntry(this);
  // 如果不为空,读取当前 ThreadLocal 中保存的值
  if (e != null) {
   @SuppressWarnings("unchecked")
   T result = (T) e.value;
   return result;
  }
 }
 // 若 map 为空,则对当前线程的 ThreadLocal 进行初始化,最后返回当前的 ThreadLocal 对象关联的初值,即 value
 return setInitialValue();
}

get 方法的主要流程为:

  • 先获取到当前线程的引用
  • 获取当前线程内部的 ThreadLocalMap
  • 如果 map 存在,则获取当前 ThreadLocal 对应的 value 值
  • 如果 map 不存在或者找不到 value 值,则调用 setInitialValue() 进行初始化

其中每个 Thread 的 ThreadLocalMap 以 threadLocal 作为 key,保存自己线程的 value 副本,也就是保存在每个线程中,并没有保存在 ThreadLocal 对象中。

ThreadLocal 的 set 方法

ThreadLocal 的 set 方法,源码如下:

Copy/**
 * 为当前 ThreadLocal 对象关联 value 值
 *
 * @param value 要存储在此线程的线程副本的值
 */

public void set(T value) {
 // 返回当前ThreadLocal所在的线程
 Thread t = Thread.currentThread();
 // 返回当前线程持有的map
 ThreadLocalMap map = getMap(t);
 if (map != null) {
  // 如果 ThreadLocalMap 不为空,则直接存储<ThreadLocal, T>键值对
  map.set(this, value);
 } else {
  // 否则,需要为当前线程初始化 ThreadLocalMap,并存储键值对 <this, firstValue>
  createMap(t, value);
 }
}

set 方法的作用是把我们想要存储的 value 给保存进去。set 方法的流程主要是:

  • 先获取到当前线程的引用
  • 利用这个引用来获取到 ThreadLocalMap
  • 如果 map 为空,则去创建一个 ThreadLocalMap
  • 如果 map 不为空,就利用 ThreadLocalMap 的 set 方法将 value 添加到 map 中

用过线程池吗?

用过。

线程池可以避免频繁创建和销毁线程所带来的开销,提高系统的响应速度和资源利用率。

如何自定义线程池?

要自定义线程池,可以使用 ThreadPoolExecutor 类。以下是创建自定义线程池的步骤:

  1. 创建一个 ThreadPoolExecutor 对象,可以通过构造方法来指定核心线程数、最大线程数、线程空闲时间、任务队列等参数。
  2. 可以通过调用 execute(Runnable command) 方法向线程池提交任务。
  3. 可以通过调用 shutdown() 方法关闭线程池。

示例代码如下:

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    5// 核心线程数
    10// 最大线程数
    60// 线程空闲时间
    TimeUnit.SECONDS, // 时间单位
    new ArrayBlockingQueue<>(100// 任务队列
);

threadPool.execute(() -> {
    // 执行任务逻辑
});

threadPool.shutdown();

线程池的参数有哪些?

线程池的构造函数有7个参数:

  • corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
  • maximumPoolSize:线程池中最多可容纳的线程数量。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程且当前线程池的线程数量小于corePoolSize,就会创建新的线程来执行任务,否则就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。
  • keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
  • unit:就是keepAliveTime时间的单位。
  • workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
  • threadFactory:线程工厂。可以用来给线程取名字等等
  • handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。

其他

  • 讲一下你项目中用过哪些设计模式?
  • 用double check实现单例模式
  • 层序换行遍历二叉树

历史好文:
跪了,米哈游是真的细节。。。
运气真好!B站给机会了?
腾讯三面:一台服务器,最大支持的TCP连接数是多少?
继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存