尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

2017阿里实习生笔试真题复盘:Java并发、JVM与算法考点全解析

2017阿里实习生笔试真题复盘:Java并发、JVM与算法考点全解析 1. 2017年这张卷子的整体风格与我的答题复盘每年三四月份互联网大厂的实习生招聘就像一场提前到来的秋招预演。阿里巴巴的实习生笔试向来以“题量大、覆盖广、深挖原理”著称2017年这一场更是如此。当时我还在读研二抱着“试水”的心态投了软件研发岗结果被那张卷子狠狠教育了一课——考完之后我做的第一件事不是对答案而是把每道题背后的知识点重新翻了一遍课本。后来我意识到那段时间的复盘比大学三年学的都扎实。先说这张卷子给人的整体印象大部分题目不是直接问你“XX是什么”而是给你一段代码、一个运行场景或者一组相似概念的选项让你判断“哪个说法正确”。这种出题方式最狠的地方在于它不考记忆考的是你对原理边界的掌握程度。比如同样一个知识点如果你只是背了结论而没理解推导过程在四个看起来都对的说法面前就会彻底懵住。从考点分布来看2017年实习笔试的常规构成大致是Java基础与集合、并发编程、JVM内存与垃圾回收、计算机网络、Linux命令与Shell、数据结构和算法、数据库SQL。其中Java和算法加起来基本占了半壁江山剩下的知识点分布在其他几块。被问到操作系统和网络的概率也很高尤其是TCP协议细节和Linux排查命令这两块几乎每年都稳定出现。考点模块大致占比常见的考查形式Java集合与基础20%左右源码细节、运行结果判断、JDK版本差异并发与多线程15%左右volatile/synchronized语义、线程池参数JVM与GC15%左右内存区域、GC Roots、垃圾回收器选型网络协议10%左右TCP握手挥手、滑动窗口、拥塞控制Linux与Shell10%左右命令作用、负载查看、文本处理数据结构与算法25%左右编程题TopK、遍历、动态规划数据库SQL5%左右连接查询、索引失效场景我在考场上最直观的感受是时间不够用。选择题每一个选项都像是精心设计过的陷阱编程题又要手写完整代码。如果你没有提前把高频考点吃透光靠临场发挥基本是送人头。下面我就按照这张卷子的考查顺序把那些让我印象深刻的题目逐个拆开讲。2. Java集合与并发HashMap底层细节和线程安全的三连问2.1 考题一HashMap的底层设计与JDK版本差异这张卷子Java部分有一道多选题大概意思是判断关于HashMap的哪些说法是正确的。选项涉及默认容量、负载因子、链表转红黑树的阈值、线程安全性以及JDK 1.7和1.8在数据结构上的差异。这道题几乎把HashMap的全部核心参数都问了一遍如果你只是知道“HashMap键值对不能重复”这种表层概念到这儿基本就歇菜了。先说底层结构。JDK 1.8之后的HashMap由数组、链表、红黑树三种结构组成。当你调用put时HashMap会先对key求哈希值然后通过扰动函数让高位也参与路由再和数组长度减一取与运算算出桶的位置。如果同一个位置上只有一个元素直接放进去就行时间复杂度O(1)。如果多个key落到同一个桶里就用链表串起来当链表长度超过8并且数组长度达到64时链表会转成红黑树把最坏情况下的查找时间从O(n)降到O(logn)。这里有一个特别容易混淆的点不是链表长度一超过8就立刻转树。代码里确实有个TREEIFY_THRESHOLD 8的常量但转树之前还有一个条件判断就是数组长度必须大于等于64。如果数组长度还没到64HashMap会优先选择扩容而不是转树。我当时答这道题的时候也犹豫了很久后来看源码确认才踏实。这个细节在《阿里巴巴Java开发手册》里也专门提到过可见官方对这个边界有多重视。再说线程安全。HashMap本身不是线程安全的它没有像Hashtable那样在每个方法上加上synchronized。在并发场景下JDK 1.7里扩张时的头插法可能会让链表形成环导致get操作死循环CPU直接飙满。JDK 1.8改成尾插法后这个问题得到了很大缓解但依然存在数据覆盖的问题两个线程同时put到同一个空桶时后写的一方可能覆盖先写的内容。所以正确的并发姿势是使用ConcurrentHashMap。对比维度JDK 1.7JDK 1.8底层结构数组 链表数组 链表 红黑树插入方式头插法尾插法哈希计算较简单高16位异或低16位扩容重新计算hash原位置或原位置旧容量并发问题扩容成环导致死循环数据覆盖不崩溃但错乱我当时就是靠着对这张表的记忆和理解答完了这道题。如果你在准备笔试我建议不只是记住这几个常量值最好自己打开JDK源码看一下put方法的完整流程看懂阈值和转换条件这样不管出题人把选项怎么组合你都能判断出来。2.2 考题二volatile、synchronized与原子类之间的关系另一道常驻题跟并发编程有关给出几段代码片段问你哪些能保证线程安全。其中一段用了volatile修饰一个int变量然后做自增操作另一段用了synchronized包裹整个方法还有一段用了AtomicInteger。这道题的陷阱在于很多人以为volatile就是“线程安全”的同义词实际上它只保证可见性和有序性并不保证原子性。为什么volatile不能保证原子性因为它只是让每个线程在读取变量时强制从主内存拿最新值在写入时立即刷回主内存。但自增操作在底层是“读-改-写”三步如果线程A读取了值x还没写回线程B也读取了同样为x的值然后两个线程各自加1再写回结果就是x只增加了一次而不是两次。这就是经典的丢失更新问题。而AtomicInteger能保证原子性靠的是CAS算法Compare And Swap比较并交换。CAS操作会先比较当前内存值和期望值是否一致一致才更新不一致就重试。这整个判断和替换过程在CPU层面是原子的所以不会出现丢失更新的问题。不过CAS也有自己的坑比如ABA问题——一个值从A变成B又变回ACAS无法感知这个过程。好在AtomicInteger对数值场景来说基本不受影响真正需要关注ABA问题的是AtomicReference针对复杂对象的场景。synchronized和volatile的关系也经常被拉来对比。synchronized是可重入的互斥锁既能保证原子性也能保证可见性。但它有一个缺点是锁的获取和释放有额外开销所以并不是所有并发场景都要上锁。业界常见的做法是对于简单的状态标记用volatile比如一个布尔变量控制线程的停止对于复合操作比如计数、累加用Atomic包里的类对于复杂的临界区逻辑才用synchronized或ReentrantLock。笔试里经常用这些概念来考你一个写法是否“真正线程安全”答的时候要记住三要素原子性、可见性、有序性缺一个都不能叫线程安全。3. JVM内存模型与GC高频考点里最容易混淆的两道题3.1 内存区域与对象生死判断JVM相关的题目几乎每次笔试都会出现2017年这批也不例外。有一道题问的是以下关于Java内存区域的描述中正确的是哪些。选项包括“堆是所有线程共享的区域”“虚拟机栈是线程私有的”“程序计数器可能抛出OutOfMemoryError”“方法区在JDK 8之后被元空间取代”。这道题本质上是考JVM运行时数据区的基础划分。先把五个区域理清楚程序计数器、虚拟机栈、本地方法栈是线程私有的堆和方法区是线程共享的。程序计数器占用的空间非常小是唯一一个在Java虚拟机规范里没有规定任何OutOfMemoryError情况的区域它只记录当前线程正在执行的字节码指令地址。虚拟机栈会在线程请求栈深度超过限制时抛出StackOverflowError在动态扩展无法申请到足够内存时抛出OutOfMemoryError。堆则是所有对象实例和数组分配内存的主要区域也是垃圾回收的重点区域。方法区在JDK 8之前叫永久代存放类元信息、常量、静态变量等JDK 8之后改为元空间并且直接使用本地内存不再受堆大小限制。这个变化是很多人在网上讨论过的热点出题人非常喜欢拿它做文章。内存区域线程共享/私有存储内容常见异常程序计数器线程私有字节码行号指示器无虚拟机栈线程私有局部变量表、操作数栈StackOverflowError / OOM本地方法栈线程私有Native方法调用StackOverflowError / OOM堆线程共享对象实例、数组OOM方法区/元空间线程共享类信息、常量、静态变量OOM还有一道关于对象是否“已死”的判断题考的是可达性分析算法。基本思路是从GC Roots出发通过引用链向下搜索如果一个对象到GC Roots没有任何引用链相连就说明它不可达可以被判定为可回收。GC Roots包括栈帧中的局部变量引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、以及本地方法栈中JNI引用的对象。这里容易混淆的是“引用计数法”和“可达性分析”的区别。引用计数法的思路很简单每个对象有一个计数器被引用一次加1引用失效减1计数器为0就回收。但Java虚拟机并没有采用这种办法因为它解决不了循环引用的问题A引用B、B引用A但这两个对象再没有被任何人使用引用计数却永远不为0导致内存泄漏。这个经典的坑在面试里被问过无数次笔试也爱吃这个概念。要记住的是目前主流的HotSpot虚拟机用的就是可达性分析。3.2 垃圾回收器选型CMS和G1怎么选JVM部分的另一道大题涉及垃圾回收器。题目大概是给你几个关于CMS和G1的说法让你判断哪些正确。我印象最深的就是G1的区域化堆内存设计它把整个堆划分成一个个大小相等的Region每个Region都可以独立扮演Eden、Survivor或者老年代的角色这种方式和传统的物理分代不一样。G1的回收过程是优先回收垃圾最多、回收收益最大的Region并且在回收时尽量做到可预期的停顿时间。CMS则是一种以最短停顿时间为目标的收集器它工作在老年代采用标记-清除算法。CMS的流程是初始标记、并发标记、重新标记、并发清除其中只有初始标记和重新标记会触发STWStop The World暂停所有用户线程并发标记和并发清除阶段可以和用户线程并行执行所以整体停顿时间很短。但CMS有两大痛点一是它使用标记-清除算法会产生大量内存碎片二是当并发阶段并发标记、并发清除跑得太快浮动垃圾来不及清理可能导致Concurrent Mode Failure这时候CMS会退化成Serial Old收集器进行Full GC停顿时间反而暴涨。这种“串行老年代兜底”的行为在实际生产环境里会把请求延迟打得很难看。所以JDK 9之后官方直接把CMS标记为废弃并用G1作为默认垃圾回收器。了解这些原理之后如果再给你一道“线上系统频繁Full GC怎么排查”的题你就不至于只能回答“调大堆内存”。你至少应该想到先用jstat看GC频率和耗时再用jmap导出堆快照接着用MAT或VisualVM分析哪些对象占用了大量内存定位到具体业务代码后再考虑是代码问题还是参数配置问题。笔试不直接考排查过程但原理清晰的人在回答选择题时会明显更稳。4. 网络与Linux面试官喜欢的场景化考点4.1 TCP连接的建立与释放网络部分的考题风格和Java部分类似都是给一个场景让你分析。有个问题是TCP建立连接时为什么需要三次握手而不是两次或者四次。这道题背后的原因要从“双方能力确认”这个角度理解。第一次握手客户端告诉服务端“我想连接你”服务端确认自己接收能力正常、客户端发送能力正常。第二次握手服务端告诉客户端“我收到了请确认我也准备连接你”客户端确认自己发送能力正常、服务端接收正常也确认了自己接收能力正常。第三次握手客户端告诉服务端“我收到了你的确认可以建立连接了”服务端确认自己发送能力正常、客户端接收能力正常。换句话说三次握手是为了让双方都确认对方的发送和接收能力都是正常的同时同步双方的初始序列号。如果只用两次握手服务端无法确认客户端的接收能力是否正常也无法保证客户端收到自己发出的同步报文如果客户端在网络上滞留了一个旧连接请求两次握手还可能导致服务端白白建立一条废弃连接浪费资源。TCP释放连接的四次挥手原理也经常被考到。发起方先发FIN表示自己数据发完了接收方回ACK确认这时候连接还能单向传输——接收方继续发数据给发起方是可以的。等接收方也发完数据后再发FIN发起方回ACK连接才彻底关闭。这个过程中先发FIN的一方会进入TIME_WAIT状态等待2MSL最长报文段寿命的两倍后才完全关闭。原因有两个一是确保最后的ACK能到达对方如果ACK丢了可以重发二是让本次连接内的所有报文在网络中消失避免干扰后续使用同样四元组的新连接。我还遇到过一个追问是SYN Flood攻击为什么难防御。本质原因是服务端收到SYN后要分配内核资源并进入SYN_RECEIVED状态等待确认攻击者不断发送伪造源IP的SYN请求但从不回复ACK服务端的半连接队列很快被塞满正常用户的连接请求就进不来了。解决思路有调整内核参数缩短SYN超时时间、启用SYN Cookie等。这种题就是典型的“你以为只考网络原理其实考你工程思维”。4.2 Linux负载查看命令Linux相关的考题里有一道看着特别简单但错误率很高的选择题以下哪个命令可以实时查看系统负载。选项给了top、free、df、ps。答案当然是top。可很多人分不清free是查看内存使用情况的df是查看磁盘空间使用情况的ps是查看进程快照的。这里我建议把常用的系统排查命令整理成一套“组合拳”笔试考到了能秒答实际工作上更有用uptime快速查看系统当前运行时间、登录用户数和负载平均值top实时监控进程状态、CPU使用率、内存使用率按P按CPU排序按M按内存排序free -h查看物理内存和交换分区的总量、已用、可用、缓存信息df -h查看各文件系统的磁盘占用情况iostat查看CPU和磁盘IO的统计信息判断是不是磁盘瓶颈vmstat 1每一秒打印一次系统整体的进程、内存、交换、CPU状态netstat -anpt / ss -lntp查看端口监听和网络连接状态负载平均值的含义也是一个容易考的点。系统负载本质上不是CPU使用率而是处于可运行状态和不可中断状态的平均进程数。Linux的负载有三个数字分别代表1分钟、5分钟、15分钟的平均值。如果这三个数字都远高于CPU核心数说明系统处于过载状态如果只是1分钟高但5分钟和15分钟很低说明只是一个短时高峰不必过于紧张。我在考场上答这类题的经验是不要死记硬背命令的名字而是把每个命令对应的“性能维度”记牢。题目问“磁盘满了怎么办”你要知道用df和du问“CPU高但load低”你要知道可能是在等待IO问“线上接口突然变慢怎么排查”你要能从网络、内存、CPU、磁盘四个维度列出排查顺序。阿里的笔试非常喜欢这种带有工程场景的题目纯背命令的反倒容易丢分。5. 算法题的取舍策略三道典型题的考场解法5.1 TopK问题的一题多解编程题部分是整张卷子里最考验基本功的地方。2017年这场的在线编程题里有一道很经典的TopK问题给定一个无序数组找出其中第K大的数。这道题目不给输入规模范围这就意味着你需要根据K和数组长度去选择最合适的解法。最直接的思路是先排序再取下标时间复杂度O(nlogn)但面试官显然不想看到这种写法。更优的方案是快速选择算法它基于快速排序的分区思想每次选一个pivot将数组分成小于pivot和大于等于pivot的左右两部分然后判断pivot的位置和K的关系只在有需要的那一侧继续递归查找。平均时间复杂度O(n)最坏情况退化为O(n^2)但是可以通过随机选pivot来避免最坏情况。public int findKthLargest(int[] nums, int k) { return quickSelect(nums, 0, nums.length - 1, k - 1); } private int quickSelect(int[] nums, int left, int right, int targetIndex) { int pivot partition(nums, left, right); if (pivot targetIndex) { return nums[pivot]; } else if (pivot targetIndex) { return quickSelect(nums, pivot 1, right, targetIndex); } else { return quickSelect(nums, left, pivot - 1, targetIndex); } } private int partition(int[] nums, int left, int right) { int randIndex left (int) (Math.random() * (right - left 1)); int pivot nums[randIndex]; swap(nums, randIndex, right); int i left; for (int j left; j right; j) { if (nums[j] pivot) { swap(nums, i, j); i; } } swap(nums, i, right); return i; } private void swap(int[] nums, int i, int j) { int temp nums[i]; nums[i] nums[j]; nums[j] temp; }另一种可行解法是维护一个大小为K的小顶堆遍历数组时如果当前元素大于堆顶就替换堆顶并调整堆最后堆顶就是第K大的元素。时间复杂度O(nlogK)空间复杂度O(K)。如果K很小或者内存有限制这种解法更实用。考场上我的经验是先把两种解法的时空复杂度都写出来再根据题目给出的规模判断用哪一种最后选择一个思路清晰的解法写到提交框里避免边写边改导致超时。5.2 二叉树的中序遍历非递归实现模板二叉树遍历也是笔试编程题的高频选手。有一道题要求用非递归方式实现中序遍历。递归写法很简单但非递归能考查你是否理解栈这个数据结构对树遍历过程的模拟。中序遍历的顺序是左子树、根节点、右子树。用一个栈模拟递归的过程从根节点出发先把所有左子节点一路压入栈然后弹出栈顶节点访问它再转到它的右子节点继续重复“压左链、弹栈、转右子节点”的循环。public ListInteger inorderTraversal(TreeNode root) { ListInteger result new ArrayList(); DequeTreeNode stack new LinkedList(); TreeNode cur root; while (cur ! null || !stack.isEmpty()) { while (cur ! null) { stack.push(cur); cur cur.left; } cur stack.pop(); result.add(cur.val); cur cur.right; } return result; }这道题的价值不只是为了准备笔试。在实际开发中比如解析表达式树、构建JSON结构、编写对一棵树形菜单的遍历逻辑这些非递归思路都能直接迁移。我后来回头看这种考基础数据结构的题目是整卷里性价比最高的代码量不大逻辑固定只要多练几次就能拿稳分。5.3 动态规划0-1背包问题的状态定义编程题里还有一道跟动态规划有关的题目我印象中考查的是0-1背包问题一堆物品每个有重量和价值背包有容量上限问能装下的最大价值是多少。这道题看着老套但非常吃状态定义和状态转移的熟练度。状态定义为dp[j]表示容量为j的背包能装下的最大价值。取第i件物品时可以选择放入或者不放入。不放入时价值是dp[i-1][j]放入时价值是dp[i-1][j-weight] value取两者较大值。用一维数组优化后内层循环必须从大往小遍历否则会重复使用当前物品多次变成完全背包问题。public int knapsack(int capacity, int[] weights, int[] values) { int[] dp new int[capacity 1]; for (int i 0; i weights.length; i) { for (int j capacity; j weights[i]; j--) { dp[j] Math.max(dp[j], dp[j - weights[i]] values[i]); } } return dp[capacity]; }考场上最怕的不是不会做而是没看到题目中的“每个物品只能取一次”这个条件直接用了完全背包的模板导致结果错得离谱。所以第一件事永远是读题把“最多/刚好装满/只能选一次/可以选多次”这些关键词勾出来再决定用哪个DP模型。0-1背包的变体很多比如分组背包、完全背包、多重背包但核心都是状态定义、状态转移、循环顺序这三样把模板吃透之后遇到新题也能快速套用。6. 从真题反推复习主线一份倒查清单复盘完这套题之后我最大的收获不是记住了几个答案而是总结出了一条复习主线。阿里的实习生笔试表面上考的是知识点实际上考的是“你有没有真正理解这门技术的前因后果、适用边界和实现细节”。所以备战的时候不要东一榔头西一棒子按照下面这张倒查清单来推进会更高效。第一层是基本功。Java集合源码、HashMap/ConcurrentHashMap的演化、synchronized和锁升级、volatile语义、线程池核心参数这些要能做到随时讲清楚不能靠死记答案。建议自己画一张Java集合框架的图把每个类的底层结构、初始容量、扩容时机、线程安全性标注出来。第二层是JVM。运行时数据区、垃圾回收算法、常见回收器、对象的创建与内存分配、类加载机制这五块属于必考内容。不要只看概念要看场景比如内存泄漏怎么排查、CMS退化成Serial Old会导致什么后果、元空间和永久代的区别为什么会影响线上配置。第三层是计算机网络和操作系统。TCP/UDP的区别、三次握手四次挥手、滑动窗口、拥塞控制、HTTP请求流程、Linux负载和IO排查命令这些都是互联网公司的通用考点。阿里的运维文化比较浓厚所以笔试里出现Linux命令相关题目很常见建议在虚拟机里把常用命令都敲一遍markdown笔记整理好随时翻。第四层是算法。剑指Offer和LeetCode高频题至少刷两遍以上按专题去刷数组、链表、栈队列、二叉树、图、动态规划、贪心、回溯、排序查找、字符串。每道题最好能掌握两种解法并讲清各自的时空复杂度。笔试的编程题往往在牛客网这类平台上做提交时会用多组测试用例跑你的代码所以边界条件、空数组、大量重复元素等情况都必须考虑到位。第五层是项目经验。笔试练的是“会做”面试练的是“会说”。哪怕你只是做了一个简单的秒杀系统或者博客管理系统也要把技术选型的原因、遇到的最大挑战、如何排查线上问题完整梳理出来。实习生候选人不需要项目有多大规模但能把项目里的每一个技术细节讲清楚比什么都强。如果你现在还在准备阶段我的建议是按照“先Java、再算法、后网络与JVM、穿插Linux与SQL”的顺序安排复习周期。每周留出固定时间做一套完整的上机模拟题严格限制时间训练自己在压力下快速定位考点的能力。我当年就是用这种方式把每道错题转换成一条知识树上的分支一个月下来覆盖了几乎所有高频考点。最后再分享一个体会笔试的题目只是工具真正值钱的是复盘时逼自己想明白的那些“为什么”。当年我在HashMap的链表转红黑树边界上卡了很久后来打开源码一行一行读那种“原来如此”的感觉比刷十道题都管用。如果你也打算冲阿里的实习岗位先把源码读起来再把这套题做透你会在真正的考场上发现那些坑你都见过。
返回列表