面试复盘 | 携程Java后端一面+二面+HR面(已意向)
一面(8.19)
聊聊你研究生的课题(智能机器人相关)
几个做的啊,分工是什么样的
如果以后想把你们做的东西商业化 产品化,怎么去收集用户的信息?
云?用户定期上传数据到云服务器,公司负责处理这些数据(不懂。朝着云的方向扯了一些)进程与线程的区别
调度:进程是资源管理分配的基本单位,线程是程序执行调度的基本单位。
切换:线程上下文切换比进程上下文切换要快得多。
拥有资源: 进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的资源。
系统开销: 创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O设备等,OS所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。线程一般存在什么地方?线程里有什么东西?
额。有没有大佬懂的评论区指导一波,当时卡住了。。说说看自己对LRU的理解。实现思路
最近最少使用机制,是操作系统中用来实现页面替换的一种优秀的算法。本质上是一种双向链表的数据结构,用哈希表来辅助维护里面的节点位置。插入时先看看哈希表里有键值冲突:没有的话,用键值新构造一个节点,插入到链表中,并移到链表头,再看看有没有越界,越了的话要把尾部节点删除;同时放入哈希表中。有的话,用新值代替旧值,然后移到链表头。若读取,判断键对应的节点存不存在,存在的话读值并把节点移到头部。其中涉及到把节点移到链表头、删除链表尾的操作。附上实现,面试官让口述:public class Solution { /** * lru design * @param operators int整型二维数组 the ops * @param k int整型 the k * @return int整型一维数组 */ class DoubleListNode { int key; int val; DoubleListNode pre; DoubleListNode next; DoubleListNode() {} DoubleListNode(int key, int val) { this.key = key; this.val = val; } } DoubleListNode head = new DoubleListNode(-1, -1); DoubleListNode tail = new DoubleListNode(-1, -1); HashMap<Integer, DoubleListNode> map = new HashMap<>(); public int[] LRU (int[][] operators, int k) { // write code here ArrayList<Integer> ans = new ArrayList<>(); head.next = tail; tail.pre = head; int size = 0; for(int i = 0; i < operators.length; i++){ int tmp_key = operators[i][1]; DoubleListNode node = map.get(tmp_key); if(operators[i][0] == 1){ if(node == null){ DoubleListNode new_node = new DoubleListNode(tmp_key, operators[i][2]); map.put(tmp_key, new_node); add2head(new_node); size++; if(size > k){ DoubleListNode weiba = removeTail(); map.remove(weiba.key); size--; } }else{ node.val = operators[i][2]; add2head(node); } }else{ // node = map.get(tmp_key); if(node == null) ans.add(-1); else{ ans.add(node.val); move2head(node); } } } int[] res = new int[ans.size()]; for(int i = 0; i < ans.size(); i++){ res[i] = ans.get(i); } return res; } void move2head(DoubleListNode node){ removeNode(node); add2head(node); } void removeNode(DoubleListNode node){ node.pre.next = node.next; node.next.pre = node.pre; } void add2head(DoubleListNode node){ node.pre = head; node.next = head.next; head.next.pre = node; head.next = node; } DoubleListNode removeTail(){ DoubleListNode weiba = tail.pre; removeNode(tail.pre); return weiba; } }
你是怎么理解线程安全的,为什么会出现线程安全的问题呢
线程安全就是说多线程访问同一段代码,不会产生不确定的结果 。
理解线程安全的两个方面:执行控制和内存可见。
执行控制的目的是控制代码执行(顺序)及是否可以并发执行。
内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。
具体的就是去保证原子性、有序性和可见性。
为什么,我当时回答的是多线程操作共享资源时无法保证原子性、有序性和可见性,面试官让结束后再思考一下,请教大佬们指点。怎么解决,有哪些方法
加锁,volatile,原子类,并发包里的一堆东西,按需所求。同步锁有什么问题啊,jdk对此做了啥改进
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与内核态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间、专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值和变量等,以便内核态调用结束后切换回用户态继续工作。
如果线程状态切换是一个高频操作时,会消耗很多CPU处理时间;对于那些需要同步的简单代码块,获取锁、阻塞挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然得不偿失的。
synchronized会导致竞争不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。
锁升级:最开始的时候处于一个无锁的状态,加锁时首先是偏向锁,当前获取到锁资源的线程,会优先让它再去获取这个锁,如果他没有获取到这个锁,就会升级到一个轻量级锁,比如用CAS来尝试获取锁,如果没有获取成功就自旋,如果自旋了一定次数后,就会升级到synchronized这个重量级锁,保证了性能。自旋锁会消耗cpu吗,为啥
线程自旋是需要消耗cpu的,说白了就是让cpu在做无用功,如果一直获取不到锁,那线程也不能一直占用cpu自旋做无用功,所以需要设定一个自旋等待的最大时间。反问:携程主要有哪些业务,对我做个简短的评价
小结:感觉这个面试官是秋招第一次面试,一开始就说:你等几分钟,我研究一下你的简历hhh,后面问问题的时候每问一个问题都要等个几十秒,嘴上还咕哝着:我想想问个什么好呢,哈哈哈太可爱了。整体上回答的还可以,问的不深。
二面(8.25)
手写一个堆排序
上来就来这有点难顶,写了二十多分中。。。一开始想用PrioityQueue偷懒的被面试官制止了(hh他笑着说:你住手),要求自己用数组模拟堆来写,真不容易,还好不久前看过(背过)。class Solution { public int[] sortArray(int[] nums) { heapSort(nums); return nums; } public void heapSort(int[] nums) { int len = nums.length - 1; buildMaxHeap(nums, len); for (int i = len; i >= 1; --i) { swap(nums, i, 0); len -= 1; maxHeapify(nums, 0, len); } } public void buildMaxHeap(int[] nums, int len) {//构建堆 for (int i = len / 2; i >= 0; --i) { maxHeapify(nums, i, len); } } public void maxHeapify(int[] nums, int i, int len) { for (; (i << 1) + 1 <= len;) { int lson = (i << 1) + 1; int rson = (i << 1) + 2; int large; if (lson <= len && nums[lson] > nums[i]) { large = lson; } else { large = i; } if (rson <= len && nums[rson] > nums[large]) { large = rson; } if (large != i) { swap(nums, i, large); i = large; } else { break; } } } private void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } }
```
- 问商城项目
简历写了一个看视频跟着做的商城网站(SSM+curd),没啥好问的,面试官问了一些开放题:怎么避免库存超卖,redis可以用在什么地方,有什么作用,怎么保证用户信息的安全等等; - 对threadLocal的理解、底层原理
ThreadLocal是 JDK java.lang 包下的一个类,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,并且不会和其他线程的局部变量冲突,实现了线程间的数据隔离。ThreadLocal的应用场景主要有:
(1)保存线程上下文信息,在需要的地方可以获取
(2)线程间数据隔离
(3)数据库连接;
底层原理:每个线程的内部都维护了一个 ThreadLocalMap,它是一个键值对数据格式,key 是一个弱引用,也就是 ThreadLocal 本身,而 value 是强引用,存的是线程变量的值。也就是说 ThreadLocal 本身并不存储线程的变量值,它只是一个工具,用来维护线程内部的 Map,帮助存和取变量。 - 使用threadLocal会出现什么问题
ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被 Entry 中的 Key 引用的,因此如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。这个时候 Entry 中的 key 已经被回收,但是 value 又是一强引用不会被垃圾收集器回收,这样 ThreadLocal 的线程如果一直持续运行,value 就一直得不到回收,这样就会发生内存泄露。 - ThreadLocal的key是哪种引用类型?为啥这么设计?
ThreadLocalMap 中的 key 是弱引用,而 value 是强引用才会导致内存泄露的问题- 若key 使用强引用:这样会导致一个问题,引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用毫无意义,如果没有手动删除,ThreadLocal 不会被回收,则会导致内存泄漏。
- 若key 使用弱引用:这样的话,引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除(清理key为null的记录),使用完了ThreadLocal最好在手动的remove一下。
- 比较以上两种情况:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候被清除,算是最优的解决方案。
- 什么是内存泄漏
内存泄漏是指用户向系统申请分配内存进行使用,可是使用完了以后却没有释放,结果那块内存用户不能访问(也许你把它的地址给弄丢了),而系统也不能再把它分配给需要的程序。 - Java有哪些引用类型?分别有哪些使用场景
- 强引用,任何时候都不会被;垃圾回收器回收,如果内存不足,宁愿抛出OutOfMemoryError
使用场景:我们平常大部分使用的场景都是使用了强引用,比如new创建对象,反射获得一个对象等。 - 软引用,只有在内存将满的时候才会被垃圾回收器回收,如果还有可用内存,垃圾回收器不会回收。
软引用可以和一个引用队列进行关联,如果这个软引用的对象被垃圾回收,就会将这个软引用加入到关联的队列中去。 可用于高速缓存。 - 弱引用(WeakReference),生命周期更短,只要垃圾回收器运行,就肯定会被回收,不管还有没有可用内存。
使用场景: 弱引用用于生命周期更短的,对内存更敏感的场景中,比如占用内存很大的Map,java api中就提供了WeakHashMap使用,就会是的大Map被及时清理掉。 - 虚引用(PhantomReference),虚引用等于没有引用,任何时候都有可能被垃圾回收。虚引用必须和引用队列联合使用,引用队列的作用和软弱引用一样。
使用场景: 我觉得他的使用场景应该在判断一个对象是否被垃圾回收了,什么时候引用队列有新的引用入队了,就说明他被回收了。
- 强引用,任何时候都不会被;垃圾回收器回收,如果内存不足,宁愿抛出OutOfMemoryError
- 反问:部门业务(火车票、机票),技术栈介绍
小结:二面主要考察的还是算法与基础知识,不过问的没那么广了,主要对ThreadLocal相关的技术原理展开,自己答的一般,平时确实没用过这个东西,只能硬着头皮去讨论,不过面试官似乎很有耐心,先等我写了好久的代码,然后还给我解释自己不会的地方,很nice
HR面(9.3)
- 自我介绍
- 项目介绍
- 未来对项目的更新迭代有哪些打算
- 跟小伙伴合作的时候出现过分歧吗?怎么解决
- 老家在哪,家庭成员
- 哪位家人对自己影响最大,为什么
- 为什么想来上海,携程对你吸引最大的点在哪里
- 手里有其他公司的offer吗
- 你对之前的两位面试官印象如何,简单评价一下(我来评价面试官?)
- 想不想知道他们对你的评价?(hh好可爱的hr)
- 反问:未来部门分配,导师制度等
总结:携程整个流程非常顺利,基本上笔试以及每一轮的面试之间都是个了一周左右。面试官看起来都像是35左右的大哥,对候选人很友好,至少态度还是很礼貌的,能感觉到对方还是有一些期待的!问的问题还算常规,一面广度,二面深度,HR面综合。(HR的声音太好听了,我说什么她都回答:好呀)
面试中遇到的问题(没回答好的问题)
- 关于线程内部有哪些东西,线程的本质以及它和jvm之间的关系
- redis在交易类系统中一般会有哪些应用,当时只回答了把热点数据存redis,应该还有很多其他的应用