目录
什么是类加载器,Java 中有哪些类加载器?它们的职责分别是什么?
什么是索引?MySQL 有哪些常见的索引类型?它们各自有什么特点?
什么是线程池?线程池有哪些核心参数?这些参数是如何影响线程池的工作机制的?
ArrayList 和 LinkedList 有什么区别?在实际应用中如何选择?
简述 TCP 三次握手和四次挥手的过程,并说明为什么建立连接是三次握手,而关闭连接是四次挥手?
二叉搜索树的性质是什么?如果在二叉搜索树中插入一个新节点,应该如何操作?
Java 中 == 和 equals 的区别是什么?
正确表述:“==” 对于基本数据类型,比较的是值是否相等;对于引用数据类型,比较的是两个对象的内存地址是否相等。而 “equals” 方法,在 Object 类中,默认也是比较内存地址,和 “==” 效果一样。但很多类(如 String 等)重写了 “equals” 方法,重写后是比较对象内容是否相等,比如 String 类重写后比较的是字符串的字符序列是否相同。
什么是类加载器,Java 中有哪些类加载器?它们的职责分别是什么?
正确内容:Java 中有三种主要的类加载器。
- 启动类加载器(Bootstrap ClassLoader):它是用 C++ 实现的,是 JVM 自带的类加载器,负责加载 Java 核心类库,比如
rt.jar
中的类,像java.lang.*
等包中的类 。它是所有类加载器的祖先,并不继承自ClassLoader
类。 - 扩展类加载器(Extension ClassLoader):负责加载
jre/lib/ext
目录下的类库以及java.ext.dirs
系统属性指定的路径中的类库 ,这些类库是 Java 平台的扩展功能。 - 应用程序类加载器(Application ClassLoader):也叫系统类加载器,负责加载应用程序的
classpath
下的所有类,我们自己编写的代码以及所依赖的第三方库等都是由它来加载。
Redis 有哪些数据结构?它们分别适用于哪些场景?
正确表述:
- String(字符串):是最基本的数据结构,不仅能存储字符串,还能存储二进制数据,比如图片、视频(不过实际应用中较少直接存大文件)。常用于缓存简单的键值对数据,例如缓存用户信息等。
- Sorted Set(有序集合):每个元素都会关联一个分数(score),通过分数来为集合中的成员进行从小到大的排序。非常适合实现排行榜功能,比如游戏中的玩家积分排行榜等。
- List(列表):基于链表结构实现,可以从两端插入和删除元素。可以用作简单的消息队列,生产者往列表的一端插入消息,消费者从另一端读取消息。
- Hash(哈希):类似于 Java 中的 HashMap,适合存储对象,以字段和值的形式存储,比如存储用户的详细信息,每个字段对应不同的属性值。
- Set(集合):无序且元素唯一。可用于实现标签功能,比如一个用户可以有多个标签,标签集合能保证不重复;也可用于统计访问网站的独立 IP 等场景。
什么是索引?MySQL 有哪些常见的索引类型?它们各自有什么特点?
正确表述:
- 索引:索引类似于书籍目录,是一种能帮助 MySQL 高效获取数据的数据结构,它能大幅提升数据库查询性能。
- 常见索引类型及特点:
- 主键索引:每个表只能有一个主键索引,主键值具有唯一性且不能为空。它基于聚簇索引结构存储,叶子节点直接存储了完整的行数据,所以通过主键查询数据速度非常快。
- 聚簇索引:并不是一种单独的索引类型,而是一种数据存储方式。聚簇索引的叶子节点就是数据节点,数据行的物理顺序与索引顺序是一致的。一张表最多只能有一个聚簇索引,通常主键索引就是聚簇索引。
- 二级索引(辅助索引):除主键索引外的其他索引都可称为二级索引。二级索引的叶子节点存储的是索引列的值以及一个指向聚簇索引中对应数据行的指针。通过二级索引查询,通常需要先找到指针,再通过指针去聚簇索引中查找完整数据行,这个过程叫回表。
- 联合索引:是多个列上创建的索引,遵循最左前缀原则,即查询时从索引的最左边开始匹配,只有在左边的列被使用时,索引才会生效。例如在
(col1, col2, col3)
上创建联合索引,WHERE col1 = 'value1' AND col2 = 'value2'
这样的查询能利用该索引,但WHERE col2 = 'value2'
就不能利用。 - 非聚簇索引:与聚簇索引相对,叶子节点不存储完整数据行,而是存储索引列和指向聚簇索引中数据行的指针,二级索引通常是非聚簇索引。
什么是线程池?线程池有哪些核心参数?这些参数是如何影响线程池的工作机制的?
正确表述:
- 线程池:线程池内部维护着一定数量的线程,可复用这些线程,减少线程频繁创建与销毁带来的资源开销,提高系统性能和响应速度。
- 核心参数及对工作机制的影响:
- 核心线程数:线程池在初始化后,会维持的最少线程数量。当有任务提交到线程池时,线程池优先创建核心线程来处理任务,即使核心线程处于空闲状态,也不会被销毁(除非设置了 allowCoreThreadTimeOut 为 true )。
- 最大线程数:线程池能容纳的最大线程数量。当任务数量超过核心线程数且阻塞队列已满时,线程池会继续创建线程,直到线程数量达到最大线程数。
- 空闲线程存活时间:当线程数超过核心线程数时,多余的空闲线程在存活时间达到该值后,会被销毁。例如,若设置空闲线程存活时间为 60 秒,一个非核心线程在 60 秒内都没有任务执行,就会被销毁。
- 时间单位:用于指定空闲线程存活时间的时间单位,如秒(TimeUnit.SECONDS)、毫秒(TimeUnit.MILLISECONDS)等。
- 阻塞队列:当核心线程都在忙碌且线程数未达到最大线程数时,新任务会被放入阻塞队列等待执行。常见的阻塞队列有 ArrayBlockingQueue、LinkedBlockingQueue 等,不同的阻塞队列特性会影响线程池性能。
- 拒绝策略:当线程池中的线程数达到最大线程数且阻塞队列已满时,新提交的任务会被拒绝,此时就会执行拒绝策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(将任务交给提交任务的线程执行)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。
- 线程工厂:用于创建线程池中的线程,可自定义线程的名称、优先级等属性,方便进行线程管理和监控。
ArrayList 和 LinkedList 有什么区别?在实际应用中如何选择?
正确表述:
- ArrayList:它是基于动态数组实现的。内存空间要求连续,查询效率高,因为可以通过数组下标直接定位元素,时间复杂度为 O (1)。但在进行添加和删除操作时,如果不是在数组末尾进行,就需要移动大量元素,时间复杂度为 O (n)。例如在中间位置插入元素,后续元素都要往后移动。另外,ArrayList 会自动扩容,当元素数量超过数组容量时,会创建一个新的更大的数组,并将原数组内容复制过去。
- LinkedList:是基于双向链表实现(不是单链集合)。内存空间不需要连续,通过节点之间的指针相连。添加和删除操作效率高,只需修改相关节点的指针指向,时间复杂度为 O (1) 。例如在链表中间插入或删除一个节点,只需修改前后节点的指针。但查询效率低,因为要从链表头或链表尾开始遍历,时间复杂度为 O (n) 。
在实际应用中的选择:
- 如果应用场景主要是查询操作,很少进行添加和删除操作,那么选择 ArrayList 更合适,比如用于存储配置信息等只读场景。
- 如果应用场景频繁进行添加和删除操作,查询操作相对较少,那么 LinkedList 更合适,比如实现一个需要频繁插入和删除元素的消息队列。
简述 TCP 三次握手和四次挥手的过程,并说明为什么建立连接是三次握手,而关闭连接是四次挥手?
正确表述:
- 三次握手过程:
- 第一次握手:客户端向服务端发送一个 SYN(同步序列编号)包,此时客户端进入 SYN_SENT 状态,这个包表明客户端想要与服务端建立连接。
- 第二次握手:服务端收到客户端的 SYN 包后,会回复一个 SYN + ACK 包,其中 SYN 是服务端发给客户端的同步信号,ACK 是对客户端 SYN 包的确认。此时服务端进入 SYN_RCVD 状态。
- 第三次握手:客户端收到服务端的 SYN + ACK 包后,再向服务端发送一个 ACK 包。服务端收到 ACK 包后,双方进入 ESTABLISHED 状态,连接建立成功。这样做是因为通过三次握手,客户端和服务端都能确认自己和对方的发送与接收能力是正常的。
- 四次挥手过程:
- 第一次挥手:假设客户端想要关闭连接,客户端发送一个 FIN(结束标志)包给服务端,表明自己没有数据要发送了,此时客户端进入 FIN_WAIT_1 状态。
- 第二次挥手:服务端收到 FIN 包后,会发送一个 ACK 包给客户端,确认收到了客户端的关闭请求,此时服务端进入 CLOSE_WAIT 状态,客户端收到 ACK 后进入 FIN_WAIT_2 状态。这里服务端可能还有数据要发送给客户端,所以不能立即关闭连接。
- 第三次挥手:当服务端数据发送完毕后,服务端向客户端发送一个 FIN 包,表明自己也没有数据要发送了,此时服务端进入 LAST_ACK 状态。
- 第四次挥手:客户端收到服务端的 FIN 包后,再发送一个 ACK 包给服务端,服务端收到 ACK 包后进入 CLOSED 状态,客户端等待一段时间(2MSL,最大报文段生存时间)后也进入 CLOSED 状态,连接彻底关闭。
- 原因:建立连接时,服务端在收到客户端的 SYN 请求后,可以同时发送 SYN 和 ACK,合并为一个包,所以三次握手即可。而关闭连接时,客户端发送 FIN 后,服务端可能还有数据未发送完,不能立刻关闭连接返回 FIN,只能先返回 ACK 确认收到关闭请求,等数据发送完再发 FIN,所以需要四次挥手。
什么是进程?什么是线程?它们之间有什么区别?
回答:进程是操作系统进行资源分配和调度的基本单位,它有自己独立的地址空间,包含了程序、数据和进程控制块等。每个进程运行在其独立的环境中,互不干扰。
线程是进程中的一个执行单元,是 CPU 调度和分派的基本单位。一个进程可以包含多个线程,它们共享进程的资源,如地址空间、文件描述符等。
它们的区别如下:
- 资源分配:进程有独立的资源,如内存空间等;而线程共享所在进程的资源。例如进程 A 中的线程 1 和线程 2 可以访问进程 A 的相同内存区域。
- 调度:进程的调度开销较大,因为涉及到资源的切换;线程调度开销小,因为线程共享资源,切换时不需要切换资源。
- 并发性:进程之间并发执行,而同一进程内的线程也可并发执行,并且由于线程共享资源,线程间通信和协作比进程间更方便高效。例如多线程的 Web 服务器可以更高效地处理多个请求。
- 健壮性:一个进程崩溃一般不会影响其他进程,但一个线程崩溃可能导致整个进程崩溃,因为线程共享进程资源。
二叉搜索树的性质是什么?如果在二叉搜索树中插入一个新节点,应该如何操作?
二叉搜索树(BST)有以下性质:
- 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值。
- 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值。
- 它的左、右子树也分别为二叉搜索树。
在二叉搜索树中插入新节点的操作步骤如下:
- 从根节点开始比较。若树为空,新节点直接作为根节点插入。
- 若新节点的值小于当前节点的值,则往当前节点的左子树走;若新节点的值大于当前节点的值,则往当前节点的右子树走。
- 重复步骤 2,直到找到一个空的位置,将新节点插入到该位置。
例如,对于一棵二叉搜索树,要插入值为 5 的节点。从根节点开始,若根节点值为 8,因为 5 小于 8,就往根节点的左子树走。如果左子节点值为 3,由于 5 大于 3,就继续往该左子节点的右子树走,若右子树为空,就将值为 5 的新节点插入到这个位置。 现在请你按照这个思路,详细描述一下插入节点操作的代码实现逻辑(不用写具体代码,只说思路)。
马戏团人塔
有个马戏团正在设计叠罗汉的表演节目,一个人要站在另一人的肩膀上。出于实际和美观的考虑,在上面的人要比下面的人矮一点且轻一点。已知马戏团每个人的身高和体重,请编写代码计算叠罗汉最多能叠几个人。
示例:
输入:height = [65,70,56,75,60,68] weight = [100,150,90,190,95,110]
输出:6
解释:从上往下数,叠罗汉最多能叠 6 层:(56,90), (60,95), (65,100), (68,110), (70,150), (75,190)
提示:
height.length == weight.length <= 10000
class Solution {
public int bestSeqAtIndex(int[] height, int[] weight) {
int n = height.length;
int[][] people = new int[n][2];
// 将身高和体重组合到一个二维数组中
for (int i = 0; i < n; i++) {
people[i][0] = height[i];
people[i][1] = weight[i];
}
// 按照身高升序排序,如果身高相同,则按体重降序排序
Arrays.sort(people, (a, b) -> {
if (a[0] == b[0]) {
return b[1] - a[1];
}
return a[0] - b[0];
});
// 提取排序后的体重数组
int[] weights = new int[n];
for (int i = 0; i < n; i++) {
weights[i] = people[i][1];
}
// 使用二分查找优化的动态规划求解最长递增子序列
return lengthOfLIS(weights);
}
private int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
int len = 0;
for (int num : nums) {
int left = 0, right = len;
while (left < right) {
int mid = left + (right - left) / 2;
if (dp[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}
dp[left] = num;
if (left == len) {
len++;
}
}
return len;
}
}
近日总结:好好学习,天天向上。
要学会擦亮双眼,不是所有的人都是好人,都有人性,也许你浅交,觉得对方是个好人,可实际上发生点什么事情,你就会发现,对方其实不是什么正常人。