ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

乐视Java实习笔试题解析:HashMap、线程池与并发底层考点全梳理

乐视Java实习笔试题解析:HashMap、线程池与并发底层考点全梳理 2017年春天那会儿我还在学校准备找暑期实习投了一圈互联网公司乐视的笔试通知来得比想象中快。收到链接的时候还有点兴奋毕竟当时乐视的生态概念铺天盖地手机、电视、视频、汽车全线开花谁都想进去看看这套玩法到底怎么运转。结果点开笔试页面倒吸一口凉气——题量不小而且不是那种随便刷刷题库就能过的水平尤其是第二套题明显比第一套更侧重于Java基础和并发底层。这套题我一直留着印象后来跟几个同样参加了笔试的同学复盘发现大家栽跟头的地方惊人地一致。趁着最近整理旧资料我把这套题背后涉及的考察逻辑、常考知识点和解题思路完整梳理一遍希望能给正在准备Java后端实习面试的同学一些实在的参考。1. 试卷整体结构与考察方向盘点1.1 题型分布与时间分配策略这套题整体分为四块选择题、填空题、编程题和简答题。选择题大概占了40分覆盖Java基础、集合框架、JVM、并发和网络填空题约20分主要抠细节比如代码运行结果、关键字的作用范围编程题两道共25分一道是算法实现一道是并发场景最后简答题15分考的是设计思路和问题排查方法。考试时间90分钟说实话挺紧张的。我当时的策略是先做选择题遇到拿不准的立刻标记跳过不纠结。填空题要留够时间因为代码运行结果这类题一旦判断错误整道题就是零分。编程题放到最后但至少要保证一道完整跑通。这个策略最后帮我保住了基本盘。1.2 这套题背后的招聘逻辑整套卷子透露出一个信息乐视当时招实习生要的不是刷题机器而是基础扎实、有工程感觉的人。这从题目设置能看出来选择题里不只有八股文还有很多“给你一段代码问哪里会出问题”的场景题考察的是真实开发中的敏感度。比如有一道关于HashMap在多线程环境下使用的选择题单纯背“HashMap线程不安全”是不够的你得知道JDK 1.7和1.8的底层实现差异理解为什么JDK 1.7扩容时可能出现环形链表而JDK 1.8改用了尾插法却依然不安全。这种题没有实际去看过源码基本只能靠猜。所以这套题表面考的是知识点实际是在筛选有没有源码阅读习惯的候选人。2. Java基础模块从语法糖到内存模型2.1 集合框架HashMap的底层到底问什么集合框架在选择题里占比很高其中HashMap又是绝对的核心。2017年那会儿JDK 1.8已经普及但很多人对1.7和1.8的区别还停留在面试题层面。这套题里有一道问“HashMap在什么条件下会触发树化”选项设计得很刁钻不单是考阈值还考链表长度和数组长度的关系。完整的答案是这样的当链表长度达到8且数组长度达到64时链表会转换为红黑树。如果数组长度没到64就算链表长度到了8也只会触发扩容把链表拆散。这个设计背后的逻辑是数组长度太小时哈希冲突本来就多与其费劲树化不如先把数组撑大从根源上减少碰撞。扩容后链表长度大概率会降下来树化也就没必要了。另外一道题考了HashMap的初始容量和负载因子。很多人记住“默认容量16负载因子0.75”但不知道为什么是0.75。这里有个统计学背景0.75是空间和时间的一个折中负载因子太高虽然省空间但哈希冲突会明显增加查询效率下降太低则浪费空间。JDK作者在泊松分布的基础上做了大量测试最终选了0.75这个值。笔试中如果能把这一层讲清楚很加分。2.2 字符串与常量池的经典陷阱字符串相关的题几乎是Java笔试的固定嘉宾这套题里有一道填空题是这样的String s1 new String(abc)创建了几个对象答案是两个一个在堆上一个在常量池里。这里要特别注意如果常量池里已经有“abc”那堆上就只有一个新对象。还有一道题让判断s1 s2的结果s1和s2分别是字面量赋值和new出来的对象。这道题的考点是引用比较和值比较的区别以及字符串常量池的复用机制。很多人在这道题上丢分是因为只记住了“比较引用equals比较值”这个口诀却没有真正理解intern方法在JDK 1.7之后的行为变化。我当时的回答方式是先说明比较的是栈中的引用地址再解释常量池的位置变化。JDK 1.7之前常量池在方法区JDK 1.7之后移到了堆中所以intern()方法的行为也随之改变。这个细节虽然看起来不起眼但能体现你对JVM内存结构演变的理解深度。2.3 异常处理与finally的执行顺序这套题里有一道关于try-catch-finally执行顺序的选择题考察return语句在finally块中的影响。题目给了一段代码try里有个returnfinally里也修改了返回变量问最终返回值是什么。这道题的坑在于Java的实现机制如果在try或catch里写了returnJVM会先执行finally中的代码然后再执行return。但如果finally里也出现了return会直接覆盖之前的返回值。更隐蔽的细节是如果finally里修改的是基本类型变量不会影响返回值因为返回值在进入finally之前就已经确定了但如果修改的是引用类型对象的内容那就会影响最终返回的结果因为引用指向的对象是同一个。比如这段代码public static StringBuilder test() { StringBuilder sb new StringBuilder(hello); try { return sb; } finally { sb.append( world); } }最终返回的结果是“hello world”因为返回的是引用finally里通过这个引用修改了对象内容。这个细节当时难倒了一批人建议准备笔试时把这几种情况自己写一遍比看十遍理论都有用。3. 并发与多线程实习生笔试的重头戏3.1 synchronized与Lock的区别与选择并发部分是这套题的压轴考点第一道简答题就是让对比synchronized和Lock的区别。这个范围很大答的时候要注意切分维度。我当时的回答分了几层第一层是语法层面synchronized是关键字自动释放锁Lock是接口需要手动加锁和解锁一般配合try-finally使用。第二层是功能层面synchronized不可中断、不可超时Lock提供了lockInterruptibly和tryLock等更灵活的能力。第三层是性能层面JDK 1.6之后对synchronized做了大量优化引入了偏向锁、轻量级锁、锁消除和锁粗化它们之间的性能差距已经不大Lock的优势更多体现在灵活性上。第四层是底层实现synchronized通过Monitorenter和Monitorexit指令实现而Lock在源码层面通过AbstractQueuedSynchronizerAQS实现支持公平锁和非公平锁。如果你能讲到AQS的state变量和CLH队列基本就能拿满分了。3.2 volatile关键字的可见性与禁止重排volatile是另一道大题的考点考察两个特性可见性和禁止指令重排序。选择题里给了一段双重检测锁的单例代码问哪里有问题。这个单例实现的问题在于Singleton instance的声明没有加volatile导致对象创建过程中的指令重排序可能让其他线程拿到一个未初始化完成的对象。问题在这段代码里public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }instance new Singleton()这行代码在字节码层面不是原子的它分为三步分配内存、初始化对象、将引用指向内存地址。如果不加volatile第三步可能被重排到第二步之前另一个线程进来发现instance不为null直接返回一个半初始化的对象。加了volatile之后通过内存屏障禁止了这种重排。这里有个细节值得说明volatile本身不保证原子性它只保证可见性和有序性。很多人把“线程安全”和“volatile”画等号这是一个很大的误区。在笔试中如果能主动点出这个误区会显得对并发理解更透彻。3.3 线程池的核心参数与拒绝策略编程题里有一道是让写一个简单的线程池或者用线程池完成一个并发任务这道题直接考ThreadPoolExecutor的七参数。七个参数分别是核心线程数、最大线程数、空闲线程存活时间、时间单位、任务队列、线程工厂和拒绝策略。关键考点是线程池的执行流程当提交一个任务时如果当前线程数小于核心线程数创建新线程执行如果达到核心线程数任务进入队列等待如果队列满了且线程数未达到最大线程数创建临时线程如果线程数已经达到最大值执行拒绝策略。很多人都背过这个流程但题目稍作变形就露馅。经典变形是问核心线程数为2最大线程数为4队列容量为8现在连续提交10个任务最终有几个线程在运行答案是2个。因为前两个任务直接创建核心线程执行后面8个任务全部进入队列队列没满不会触发额外线程创建。很多人看到最大线程数为4就下意识认为会创建4个线程忽略了队列的空间。拒绝策略也有四个AbortPolicy直接抛异常、CallerRunsPolicy由调用线程执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃队列中最老的任务。其中CallerRunsPolicy在生产环境用得较多因为它在任务被拒绝时能让调用线程自己执行不会丢失任务还能通过慢下来给系统降压。4. 数据结构与算法不背题也能过的思路4.1 链表反转的递归与迭代两种写法算法这块有两道题第一道是链表反转非常经典的题目。这道题虽然基础但考察点很细面试官期望你能给出两种解法迭代和递归。迭代法思路很直接用三个指针prev、current、next边走边反转。我当时的实现public ListNode reverseList(ListNode head) { ListNode prev null; ListNode current head; while (current ! null) { ListNode next current.next; current.next prev; prev current; current next; } return prev; }递归法稍微难理解一点核心是先把后面的链表反转再处理当前节点。关键在于理解递归的返回值永远是反转后的新头节点。public ListNode reverseList(ListNode head) { if (head null || head.next null) { return head; } ListNode newHead reverseList(head.next); head.next.next head; head.next null; return newHead; }这道题想拿满分还需要分析时间和空间复杂度。两种方法的时间复杂度都是O(n)但迭代法空间复杂度是O(1)递归法因为栈空间的消耗是O(n)。笔试中如果时间允许建议把两种写法都写上不要只写一种。4.2 排序算法的时间复杂度与稳定性对比这套题的选择题里有一道关于排序算法的表格题要求选出排序算法、时间复杂度和稳定性完全正确的一项。这道题虽然只有一分但涉及的内容非常多需要你熟悉七八种常见排序的全部特征。这里把常考的几种排序罗列一下冒泡排序平均O(n²)稳定选择排序平均O(n²)不稳定最大的问题是不管数据有序与否都会进行固定次数的交换插入排序平均O(n²)稳定在数据基本有序时效率很高归并排序平均O(n log n)稳定但需要O(n)的额外空间快速排序平均O(n log n)最坏O(n²)不稳定堆排序平均O(n log n)不稳定。我当年在这道题上差点出错因为把快速排序的不稳定性记成了稳定。实际上快速排序在partition过程中元素的相对顺序可能被打破比如基准元素和另一个相等元素的位置会发生交换。如果对稳定性判断没把握最简单的检验方法就是拿一个重复元素的数组走一遍算法看看相等元素的顺序有没有改变。4.3 二叉树的层序遍历与变体第二道算法题是二叉树的层序遍历也就是按从上到下、从左到右的顺序输出节点值。这道题的常规解法是用队列广度优先遍历每层结束时做一个标记。我当时的实现用了一个trick在每轮循环开始前先记录队列的长度这个长度就是当前层的节点数。public ListListInteger levelOrder(TreeNode root) { ListListInteger result new ArrayList(); if (root null) return result; QueueTreeNode queue new LinkedList(); queue.offer(root); while (!queue.isEmpty()) { int size queue.size(); ListInteger level new ArrayList(); for (int i 0; i size; i) { TreeNode node queue.poll(); level.add(node.val); if (node.left ! null) queue.offer(node.left); if (node.right ! null) queue.offer(node.right); } result.add(level); } return result; }笔试中一般不会只考基础遍历而是会出变体比如之字形遍历也就是ZigZag顺序。这个变体只需要在层序遍历的基础上加一个level变量奇数层从左到右偶数层从右到左实现方式可以是用LinkedList的头插法也可以收集完一层后逆序。考察的还是对队列使用和层信息维护的掌握程度。5. 网络与数据库容易被忽视的送分题5.1 TCP三次握手与四次挥手的过程拆解网络部分的题不多但考得很细。有一道选择题问TCP三次握手中第二次握手服务端发送的SYN和ACK标志位分别代表什么。这题看似简单其实考的是对握手语义的深层理解。三次握手的本质是确认双方的收发能力服务端在第二次握手中发送SYNACKSYN表示“我收到了你的SYN我也准备好建立连接了”ACK表示“我确认了你的序号”。客户端收到后只有再次回一个ACK才能让服务端确认“客户端的接收能力正常”因为此时服务端发送的SYN还没有得到确认。我常用一个生活里的例子来说三次握手就像两个人打电话确认互相听得到对方第一次试探第二次回应加确认第三次最终确认缺一次都没法确保双方都准备好。四次挥手也要理解为什么需要四次因为TCP是全双工的每一方的关闭都需要单独确认。主动关闭方发送FIN后被动方可能还有数据要发送所以ACK和FIN是分开的这就是为何挥手需要四次而不是三次。5.2 SQL索引失效的典型场景数据库部分考了一道SQL优化题给了一条慢查询让分析原因并给出优化方案。这道题的关键在于发现WHERE条件里对索引列用了函数导致索引失效。经典的索引失效场景包括对索引列使用函数或表达式、使用LIKE以通配符开头的模糊查询、隐式类型转换导致索引失效、OR条件中存在非索引列、不满足最左前缀原则。笔试中如果能列出这些场景再对着题目逐条排查基本不会失分。比如这道题里WHERE DATE(create_time) 2017-06-01这种写法即使create_time上有索引也用不上。优化方式是改为范围查询WHERE create_time 2017-06-01 00:00:00 AND create_time 2017-06-02 00:00:00这样就能走索引了。这个改法在选择题里经常作为正解出现。5.3 事务隔离级别与脏读幻读有一道简答题关于数据库事务的隔离级别要求说出四种隔离级别以及分别解决了什么问题。这个知识点不难但因为平时写代码很少关心很多人答不完整。四种隔离级别读未提交允许脏读、读已提交避免脏读但可能出现不可重复读、可重复读避免脏读和不可重复读但可能出现幻读、串行化全部解决但并发性能最差。MySQL默认的可重复读通过MVCC和间隙锁在某种程度下也解决了幻读问题。这道题容易丢分的地方在于很多人分不清不可重复读和幻读的区别。不可重复读是同一行数据在事务中两次读取的结果不同因为其他事务修改了这行数据幻读是同一条件下两次查询返回的记录数量不同因为其他事务插入或删除了记录。这两个概念在面试中经常被追问建议一定要用自己的话讲清楚。6. 开放性设计与逻辑思维题6.1 设计一个线程安全的单例简答题最后一题是设计一个线程安全的单例模式并说明为什么选这种方案。这道题看似开放其实考察的是对并发工具类和JVM机制的掌握至少要能说出三种以上方案的优劣。最简单的方案是饿汉式通过类加载机制天然保证线程安全但问题在于不支持懒加载类加载时就创建了实例。懒汉式加synchronized虽然线程安全但每次都加锁高并发场景下性能不佳。双重检测锁需要配volatile这个方案综合了懒加载和线程安全是笔试中的首选答案。此外还可以用静态内部类方式利用类的加载机制保证线程安全同时支持懒加载。最后一种是枚举单例这是《Effective Java》推荐的写法天然线程安全且能防止反序列化破坏单例。我当时的回答选了双重检测锁因为它在理解门槛和实际可用性之间取得了平衡。但我也补充了静态内部类方案因为那是开发中最常用的写法。答开放题的时候不要只给一个答案把自己知道的其他方案做一个横向对比得分会明显更高。6.2 经典的“三个线程轮流打印”问题还有一道编程拓展题让三个线程轮流打印ABC每个线程打印自己的字母循环三遍。这道题考察的是线程协作和状态控制网上有很多种解法但我建议掌握两种因为面试官可能会要求你换方式实现。第一种是用synchronized wait/notify控制状态变量。每个线程循环里检查当前状态是否是自己负责的数字是就打印并更新状态、唤醒其他线程。要注意配合while循环而不是if来检查条件这是为了避免虚假唤醒这也是wait/notify使用的经典注意事项。第二种是用Lock Condition每个线程一个Condition通过signal精确唤醒下一个线程比notifyAll更高效因为notifyAll会唤醒所有线程而其他线程醒来后发现自己不该执行又回去等待白白浪费一次上下文切换。这道题的变体还有“四个线程交替打印1到100”。我建议把这两种基础解法练熟理解了状态机之后就很好举一反三。这道题我在笔试时没有写完后来复盘才发现思路不难主要栽在了对Condition API的不熟练上。如果有同学还在准备面试这个知识点值得多写几遍。7. 常见问题与排查技巧实录7.1 笔试中常见的失分点回顾这套题我发现失分点往往不在最难的题而在那些“看似简单但容易想当然”的地方。第一个失分点是集合框架中HashMap树化阈值的边界条件很多人记得“8”但忘了数组长度的限制这类细节需要看源码才能准确记住。第二个失分点是线程池参数的实际分配流程特别是队列未满时不会创建临时线程这一点。原因在于大家学线程池时背的是结论而不是执行顺序一旦题目稍微绕了一点就按照直觉去推。我的建议是自己在本地写一个带打印日志的线程池把每个任务的执行路径输出出来亲眼看到结果比背十遍都管用。第三个失分点是SQL优化题只会说“加索引”而不会分析原因。比如索引失效的原因如果答不出函数操作和隐式转换阅卷老师就会觉得你只是背了面试题没有真正理解B树的查找逻辑。7.2 我的避坑建议与准备思路针对这套2017年的笔试题我给当时正在准备实习的同学几个实在的建议。第一个建议是刷题不要只看面经一定要动手写。特别是并发相关的题目看十遍不如跑一遍Java的并发问题非常依赖visibility和ordering只有实际写出多线程代码才能理解那些看起来抽象的概念。第二个建议是准备面试时要把知识串成网络孤立的知识点很容易忘。比如JVM内存分区和Java并发中的可见性问题其实是关联的线程共享主内存每个线程有自己的工作内存volatile通过缓存一致性协议实现可见性而这又和计算机组成原理中的MESI协议相关。把知识串起来之后考什么都难不倒你。第三个建议是做完题一定要复盘尤其是那些做错的题。我当年在这套题里丢分最多的就是线程池执行流程和SQL索引失效后来花了整整一天时间把这两个知识点从原理层面彻底打通。笔试只是一个过滤器真正让你和别人拉开差距的是考完之后有没有把不懂的地方补上。这套题整体问得很扎实考查的大多不是冷门知识而是基础中的关键细节。现在回看2017年正是互联网公司大量扩招实习生的年份笔试题目质量参差不齐乐视这套题算是用心出的。如果你正在准备类似岗位的面试把这套题考察的知识点逐个弄明白比盲目做一百道LeetCode有用得多。
返回列表