title: 2021年4月12日 阿里供应链Java研发实习面试(一面)
tags: 面经
2021年4月12日 阿里供应链Java研发实习面试(一面)
自我介绍
介绍项目
你用哪个版本的Java呢?(Java8)
那你觉得Java8中有哪些新的特性呢?(lambda)
try{}catch(){}finally{},如果在catch当中就已经return了,那么finally中的代码还会执行吗?(当然会的)
你了解的Map集合有哪些,他们之间有什么区别呢?(HashMap、HashTable、HashSet、concurrentHashMap)
你能说一下CAS是什么意思吗?(这个是因为在讲Map的时候,
讲到了ConcurrentHashMap的保证是通过synchronized和CAS来保证的,所以就被问到CAS的原理了)(见JavaGuide)
JVM当中的内存结构分为什么?分别有什么作用?(见JavaGuide)
GC机制的GC算法有哪些,以及他们的适用场景(见JavaGuide)
Tcp和http的区别?(这个问题还是第一次被问到,常规套路不应该是问tcp和udp的区别吗。。。。)
(但是,这个问题不难的,就是http是在应用层,而tcp是在传输层的,http请求是需要运行在建立好的tcp连结上的)
tcp是如何保证可靠传输的?(见JavaGuide)
udp是如何保证可靠传输的?(见下文)
http的长连接和短连接区别,应用场景,(感觉这个问题经常会被问)(见JavaGuide,但是那个上面感觉讲的不是很详细,还得自己总结)(见下文)
你觉得你手机中那个app是使用长连接,哪个app使用短连接(我觉得着这个问题就是个坑,稍有不慎就会掉入坑中。。。)
cookie和session的区别,作用(见JavaGuide)
操作系统当中内存管理机制(页式管理、段氏管理、段页式管理)(见JavaGuide)
虚拟内存你了解吗?(见JavaGuide)
什么事务呢,它有哪些特性呢?(ACID)(见JavaGuide)
那再具体说说什么是隔离性?(见JavaGuide)
如果有一个已经排好序的数组,采用堆排序的时间复杂度是多少呢?(O(nlogn))为什么呢?以及适用的场景。
能说一说堆排序的过程吗?
手撕算法题
反问环节
1.1 CAS的原理
参考博客:Java:CAS(乐观锁)
CAS的英文单词为Compare And Swap的缩写,翻译过来就是比较并交换。CAS是一种乐观锁。在CAS当中使用3个基本操作,分别为:内存地址V、旧的预期值A、要修改的新值B。当更新一个变量的时候,只有当前的变量预期值A 和 内存地址中的实际的值相同时,才会将内存地址对应修改为要新值B。
例如,在内存地址V当中,假设存放着一个变量D为10的值,此时,线程1想要去修改变量D。对于线程1来说,旧的预期值A=10,而要修改的新值B=11。如果在线程1要提交更新前,线程2获取到cpu的时间片,将内存地址V中的值率先更新为11,而再次当线程1获得cpu时间的时候,准备提交更新的时候,首先会进行旧的预期值A和内存地址V当中的实际值比较,如果此时发现A不等于V的实际值,则就提交失败,线程1就只能再重新获取内存地址V上的值,而此时旧的预期值A=11,新值B=12,等到没有其他的线程去更改内存地址V中的值,这个时间就会把内存地址V中的值替换为B,也就是12。这样采用CAS机制,就保证了原子性操作,保证了线程安全性。
1.2 udp是如何保证可靠传输的?
udp不属于连结协议,具有资源消耗较少,处理的速度快的优点。但是,我们都知道udp是传输层是不可靠的连接的,所以我们是没有办法去保证udp在传输层实现数据的可靠传输的。但是,我们只能通过 ==应用层==来 实现了。实现的方式可以参照tcp可靠性传输的方式,只是说实现不在传输层,转移至应用层上了。
具体的实现方式:
① 添加seq/ack机制,确保数据发送到对方;
② 添加发送和接收缓冲区,主要是为了用户的超时重传;
③ 添加超时重传机制。
过程:发送方发送数据时,生成一个seq=x,然后每一片按照数据大小分配seq。数据达到接收方后放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到了,定时任务检查是否需要重传数据。
1.3 http的长连接和短连接区别,应用场景
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
**短连接:**就是说,浏览器和服务器每进行一次Http操作,就会建立一次连接,但是任务结束后就会中断连接;
**长连接:**在使用长连接的情况下,当打开一个网页完成后,客户端和服务器之间用于HTTP数据的Tcp 连接不会关闭。
区别:
① 服务器端空间管理上:
Keep-Alive不会永久保持连接,因为TCP连接将会越来越多,直到把服务器的TCP连接数量撑爆到上限为止,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间;
短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。
② 时间上:
在客户请求频繁的情况下:若使用短连接,将在TCP的建立和关闭操作上浪费时间和带宽;
若使用长连接,就可以节省很多这样的消耗;
使用场景:
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连接好。
长连结的缺点:
在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,从而导致服务器崩溃。
1.4 HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
**短连接:**就是说,浏览器和服务器每进行一次Http操作,就会建立一次连接,但是任务结束后就会中断连接;
**长连接:**在使用长连接的情况下,当打开一个网页完成后,客户端和服务器之间用于HTTP数据的Tcp 连接不会关闭。
区别:
① 服务器端空间管理上:
Keep-Alive不会永久保持连接,因为TCP连接将会越来越多,直到把服务器的TCP连接数量撑爆到上限为止,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间;
短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。
② 时间上:
在客户请求频繁的情况下:若使用短连接,将在TCP的建立和关闭操作上浪费时间和带宽;
若使用长连接,就可以节省很多这样的消耗;
使用场景:
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连接好。
长连结的缺点:
在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,从而导致服务器崩溃。
1.5 堆排序
如何理解"堆"?
①堆是一个完全二叉树;完全二叉树要求,除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。因为我们不需要存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。
②堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做**“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做“小顶堆”**。
如何实现堆排序?
堆排序的过程大致分解成两个大的 步骤:建堆和排序。
① 建堆(从下往上和从上往下)(建堆的时间复杂度是O(n))
第一种建堆的实现思路:
第一种思路:从前往后处理数组数据,并且每个数据插入堆中时,都是从下往上堆化
第二种建堆的实现思路:
第二种思路:是从后往前处理数组,并且每个数据都是从上往下堆化。
② 排序 (排序过程的时间复杂度是O(nlogn))
当堆顶元素移除之后,我们把下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。
因此,堆排序是一种原地的、时间复杂度为O(nlogn)的排序算法,但是,堆排序是一个不稳定的排序算法。它的最好时间复杂度,最坏时间复杂度以及平均时间复杂度都为O(nlogn)。
适用场景:
① 优先级队列
假设我们有 100 个小文件,每个文件的大小是 100MB,每个文件中存储的都是有序的字符串。我们希望将这些 100 个小文件合并成一个有序的大文件。这里就会用到优先级队列。
整体思路有点像归并排序中的合并函数。我们从这 100 个文件中,各取第一个字符串,放入数组中,然后比较大小,把最小的那个字符串放入合并后的大文件中,并从数组中删除。
假设,这个最小的字符串来自于 13.txt 这个小文件,我们就再从这个小文件取下一个字符串,放到数组中,重新比较大小,并且选择最小的放入合并后的大文件,将它从数组中删除。依次类推,直到所有的文件中的数据都放入到大文件为止。
② topK问题
如果每次询问前 K 大数据,我们都基于当前的数据重新计算的话,那时间复杂度就是 O(nlogK),n 表示当前的数据的大小。实际上,我们可以一直都维护一个 K 大小的小顶堆,当有数据被添加到集合中时,我们就拿它与堆顶的元素对比。如果比堆顶元素大,我们就把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理。这样,无论任何时候需要查询当前的前 K 大数据,我们都可以立刻返回给他。
③ 利用堆动态地求中位数
我们需要维护两个堆,一个大顶堆,一个小顶堆。大顶堆中存储前半部分数据,小顶堆中存储后半部分数据,且小顶堆中的数据都大于大顶堆中的数据。
也就是说,如果有 n 个数据,n 是偶数,我们从小到大排序,那前 n/2 个数据存储在大顶堆中,后 n/2 个数据存储在小顶堆中。这样,大顶堆中的堆顶元素就是我们要找的中位数。如果 n 是奇数,情况是类似的,大顶堆就存储 n/2+1 个数据,小顶堆中就存储 n/2 个数据。
在实际开发当中,为什么快速排序比堆排序性能好?
① 堆排序数据访问的方式没有快速排序友好。
对于快速排序来说,数据是顺序访问的。而对于堆排序来说,数据是跳着访问的。比如,在堆排序中,最重要的一个操作是数据的堆化。例如,我们需要对堆顶结点进行堆化,会依次访问数组下标是1,2,4,8的元素,而不是像快排那样,局部顺序访问,所以对CPU缓存是不友好的。
② 对于同样的数据,在排序的过程中,堆排序算法的数据交换次数要多于快速排序。
在排序算法中,有两个概念:有序度和逆序度。对于基于比较的排序算法来说,整个排序过程就是由两个基本的操作组成,即比较和交换(移动)。快速排序的数据交换次数不会比逆序度多。
堆排序的第一步是建堆,建堆的过程中会打乱数据原有的相对先后顺序,导致原数据的有序度降低。比如,对于一组已经有序的数据来说,经过建堆之后,数据反而变得更无序了。
手撕算法题:
//评测题目:
// 给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
// 数组中的每个元素代表你在该位置可以跳跃的最大长度。
// 判断你是否能够到达最后一个下标。
// 示例 1:
// 输入:nums = [2,3,1,1,4]
// 输出:true
// 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
// 示例 2:
// 输入:nums = [3,2,1,0,4]
// 输出:false
// 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
//
//这里才是正确答案,也是当时手撕出来的
//public static boolean Jump(int[] nums){
// int n = nums.length;
// int r = 0;
// for(int i = 0; i < n; i++){
// if(i <= r){
// r = Math.max(r, i + nums[i]);
// if(r >= n - 1){
// return true;
// }
// }
// }
// return false;
// }
//这里是面试官更改了一下题,再次手撕的代码
// step count
public static int Jump(int[] nums){
int n = nums.length;
int r = 0;
int count = 0;
for(int i = 0; i < n; i++){
if(i <= r){
r = Math.max(r, i + nums[i]);
count++;
if(r >= n - 1){
//return true;
return count;
}
}
}
//return false;
return -1;
}