尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

2015新浪微博Java笔试题复盘:高并发场景下的Java核心考点

2015新浪微博Java笔试题复盘:高并发场景下的Java核心考点 2015年这个时间点国内互联网公司校招笔试基本还处在纸质试卷手写代码的阶段。我当时做完新浪微博这套Java研发工程师笔试题出考场最大的感受是这套题没有一道是背了就得分的偏题怪题但每一道都能筛掉一批人。十年后再翻出来看反而更清楚它想考察的是什么——不是你会多少框架API而是你有没有真正理解Java这门语言在大型高并发系统里不能做什么。这篇文章把当年的真题结构、每一个模块的典型题目、考后复盘的正确思路完整梳理出来。适合两类人一类是正在准备校招或跳槽的Java工程师可以拿它当查漏补缺的清单另一类是带团队、出面试题的技术负责人可以参考这套卷子来设计自己团队的筛选标准。题目本身的答案固然重要但更值得琢磨的是为什么新浪微博要出这道题——它背后对应的是真实业务里的什么问题。1. 从真题结构反推候选人能力画像微博要的不是会写Java的人1.1 一场笔试的真实构成四类题型两个半小时根据当年参加过笔试的同学回顾新浪微博2015校园招聘Java研发工程师笔试题型分为四部分单选题、多选题、简答题和编程题一共大约30道左右考试时长两个半小时。单选题多选每题1到2分简答题每题5到10分编程题每题15到20分整卷满分100分。这个分值分布本身就透露了一个信息基础选择判断部分撑死了占30%真正拉开差距的是简答题和编程题。也就是说如果你只会背概念、刷选择题即使前面全对总分也很难过线。我自己当年犯的错就是在前面的选择题上反复纠结最后编程题只剩二十分钟手忙脚乱只写完一题半。后来跟几个拿到offer的同学交流他们普遍的做法是先花五分钟扫一遍全卷把编程题的时间死死锁在五十分钟以上。1.2 题目背后隐藏的业务场景高并发是永恒主线把整张卷子拼起来看有一个非常清晰的指向所有考点都在为高并发、大流量、海量数据服务。HashMap的并发问题对应的是缓存系统在高并发写入时的安全volatile和线程池对应的是接口层如何扛住瞬时流量JVM内存分配对应的是服务端长时间运行如何避免OOM设计题直接考察feed流这种典型的微博核心场景。这不是巧合。新浪微博的Java服务端面临的挑战和普通企业级应用完全不同用户量过亿、热点事件导致流量峰值陡增、feed流写入读取比例极其悬殊。所以笔试不是在考你会不会Java而是在考你有没有想过Java在极端场景下怎么死。这也是为什么这套题放到今天依然值得做——虽然技术栈已经从SSH演化到Spring Cloud、Kubernetes但并发控制、缓存设计、内存管理这些底层问题从来没有变过。1.3 知识模块权重别再相信框架为王根据我对这套题以及同时期其他大厂笔试题的整理考点权重大致如下表所示。你可以对照自己的复习计划看看有没有配比失衡。考察模块大致占比典型题目考察目的Java语言基础20%String创建对象、装箱拆箱考察语言底层是否扎实集合框架15%HashMap原理、ArrayList扩容考察日常编码是否理解数据结构并发编程15%volatile、synchronized、线程池考察高并发场景基础能力JVM15%内存区域、类加载、OOM考察线上问题排查意识Spring/MySQL10%Bean作用域、事务隔离级别考察主流技术栈熟练度网络/设计10%HTTP协议、feed流设计考察架构思维和全局观算法与编程15%链表反转、快排考察代码基本功你现在看这个表可能觉得平淡无奇但放在2015年很多培训机构出来的候选人只刷框架、完全不懂JVM一套选择题下来就原形毕露了。基础不牢后面爬得越高摔得越重这套题的筛选逻辑到今天依然是成立的。2. 客观题高频考点精讲每一道题都对应一个线上事故2.1 HashMap与并发一张表讲清JDK 7和JDK 8的生死差别真题里有一道非常经典的多选题关于HashMap下列说法正确的是。选项涉及初始容量、负载因子、键值对要求、线程安全性。这道题放在2015年答案里必然有一项是多线程环境下使用HashMap可能导致CPU 100%。为什么新浪微博要考这个因为当时他们线上系统真的出过这种事故。JDK 7及以前的HashMap在多线程并发put时如果触发扩容多个线程同时执行transfer方法头插法会把链表节点顺序调转极端情况下形成环形链表。之后任何线程get这个key都会在环里无限循环CPU直接飙满。这个坑的根因去翻JDK 7的源码void transfer(Entry[] newTable, boolean rehash)里面是倒序遍历旧链表并头插到新表一旦两个线程交叉执行就完全乱套了。所以复习HashMap不能只背数组加链表、默认初始容量16、负载因子0.75还要理解几个关键点HashMap允许key和value为null但Hashtable和ConcurrentHashMap不允许。默认初始容量16和负载因子0.75决定了什么时候扩容当已有元素数量超过容量 * 负载因子时容量翻倍。为什么容量总是2的幂次因为(n - 1) hash比取模运算更快且能保证索引分布均匀。JDK 8之后链表长度超过8且数组长度超过64转为红黑树从O(n)降到O(logn)。特性JDK 7JDK 8数据结构数组链表数组链表红黑树插入方式头插法尾插法hash扰动4次异或1次高16位异或并发问题可能死循环可能数据丢失JDK 8虽然解决了死循环问题但并发下依然可能丢数据所以实际应用中高并发场景一律用ConcurrentHashMap。这道题给我们的启示是每种集合类的线程安全性不是靠背而是靠理解它底层的实现机制。2.2 volatile与JMM可见性不等于原子性的经典陷阱真题里的另一道高概率题关于volatile关键字以下说法正确的是。正确选项只有一个核心volatile保证可见性和禁止指令重排序但不保证原子性。这个考点在2015年尤其重要因为当时很多Java开发者已经把volatile当成万能并发钥匙。实际上它只解决两个问题多个线程之间的可见性以及编译器/CPU层面的指令重排。以最经典的例子说明public class Counter { private volatile int count 0; public void increment() { count; // 不是原子操作 } }count在字节码层面其实是三步读取count的值、将值加1、把新值写回。volatile只能保证每次读取的都是最新值但两个线程可能同时读到相同的旧值各自加1后再写回结果只增加了1。这就是可见性不等于原子性最直观的体现。正确的并发计数方案是使用AtomicInteger的CAS操作或者给整个方法加synchronized。这道题背后的真实业务含义是在高并发系统里如果状态标记用错了并发原语轻则统计失真重则触发雪崩。后来很多面试官都喜欢往下追问volatile和synchronized的区别如果当时只答出一个轻量一个重量基本就凉了。完整的答法是volatile只能修饰变量synchronized可以修饰方法、代码块、类、变量。volatile不保证原子性synchronized保证原子性。volatile不会引起线程阻塞synchronized会。volatile本质是告诉JVM该变量在工作内存中的副本失效重新从主内存读取synchronized本质是锁。2.3 JVM内存区域程序计数器为什么永远不会OutOfMemoryError单选题里有一道JVM中哪个内存区域不会抛出OutOfMemoryError答案是程序计数器Program Counter Register。这道题考的是对JVM运行时数据区的完整理解。JVM内存分为线程私有和线程共享两大类。程序计数器、虚拟机栈、本地方法栈是线程私有的堆、方法区JDK 8以后改为元空间是线程共享的。程序计数器是当前线程所执行的字节码行号指示器每条线程都有自己独立的一个且它占用的空间极小JVM规范规定它是唯一不会OOM的区域。这道题的扩展考点非常多比如栈溢出和堆溢出的区别栈溢出StackOverflowError通常是递归调用没有退出条件每次调用都会创建栈帧栈帧深度超过JVM允许范围。堆溢出OutOfMemoryError: Java heap space通常是创建对象太多堆空间耗尽。2015年那会儿JVM参数调优还是面试热点比如-Xms、-Xmx、-XX:MaxPermSize。到了今天虽然G1取代了CMS成为主流垃圾收集器但内存区域的划分原理完全没变。建议复习时把《Java虚拟机规范》里的运行时数据区图背下来面试时能徒手画出来这就是加分项。2.4 Spring与MySQL的送分题为什么这种题反而容易丢分简答或选择里会穿插Spring和MySQL的题目难度不高但正确率往往很低。比如Spring默认的Bean作用域是什么很多人想都不想就选prototype因为觉得多例更安全。正确答案是singleton单例。这背后对应的是Spring IOC容器的工作原理默认情况下容器里每个Bean只有一个实例好处是节省内存、提升性能坏处是如果Bean里面维护了可变状态就会出现并发安全问题。2015年那会儿还有一道相关的经典问法singleton作用域的Bean出现线程安全问题怎么办标准答案三板斧把可变状态改为方法局部变量、使用ThreadLocal、加锁。但如果再加一句尽量设计成无状态Bean面试官会觉得你有实战经验因为无状态Bean才是Spring在高并发场景下的最佳实践。MySQL的送分题是InnoDB默认的隔离级别是什么答案是可重复读REPEATABLE READ。大多数人只背了隔离级别名称没有想过为什么MySQL默认是可重复读而不是读已提交。背后的逻辑牵扯到InnoDB的MVCC机制和Binlog的格式在主从复制场景下如果隔离级别是读已提交会导致Binlog中记录的变更顺序和实际执行顺序不完全一致而可重复读配合间隙锁能更好地解决幻读问题。这类需要追根因的知识点正好是笔试拉开差距的地方。3. 简答题实战高分答案需要结论过程场景3.1 字符串创建对象数量一道让大批人翻车的计数题简答题有一道几乎必考的经典题String s new String(abc) 创建了几个对象答案不是唯一的一个而是1个或2个。具体分析执行这行代码时如果字符串常量池中已经存在abc那么只会在堆中创建一个String对象如果常量池中不存在abc那么JVM会先在常量池中创建这个字符串字面量然后再在堆中创建一个String对象总共2个。这个考点的进阶问法是为什么String被设计成不可变类。参考答案是字符串常量池复用、缓存hashCode、安全性防止被篡改、线程安全。但最贴近新浪微博业务场景的回答是在大量数据读写的系统中不可变对象可以被安全地多线程共享不用考虑同步问题。缓存系统里最常见的Key类型就是String如果String是可变的整个缓存体系都会崩掉。3.2 线程池参数与处理流程答到拒绝策略才算深度扫码看题面Java线程池的核心参数有哪些当一个任务提交给线程池时执行流程是什么这道题在2015年考过多次在今天依然是最高频的并发题目之一。一个标准的执行流程可以概括为五步当线程池收到新任务时如果当前运行的线程数少于corePoolSize创建新线程执行任务。如果当前线程数大于等于corePoolSize将任务放入阻塞队列。如果队列已满且运行线程数小于maximumPoolSize创建新线程执行任务。如果队列已满且运行线程数达到maximumPoolSize通过饱和策略拒绝任务。当线程空闲时间超过keepAliveTime且当前线程数大于corePoolSize回收线程。参数含义典型设置建议corePoolSize核心线程数IO密集型任务设置CPU核数*2maximumPoolSize最大线程数根据压测峰值确定keepAliveTime空闲线程存活时间通常30~60秒workQueue任务队列有界队列更安全threadFactory线程工厂自定义名称方便排查rejectedExecutionHandler饱和策略根据业务选择这里特别推荐回答时提一下当任务队列使用无界队列时maximumPoolSize将失去意义因为队列永远不会满。新浪微博这种高并发系统绝对不会用无界队列否则瞬时大量任务堆积会把内存打死。这道题考察的已经不是API层面的记忆而是你对线程池运行机制和业务承载能力的整体理解。3.3 类加载过程与双亲委派概念题背后的安全考量简答题里出现过简述JVM类加载过程以及什么是双亲委派模型。类加载过程分为加载、验证、准备、解析、初始化五个阶段。加载是通过全限定名获取类的二进制字节流将其转化为方法区运行时数据结构并在堆中生成Class对象验证是确保字节流符合虚拟机规范准备是为类变量分配内存并设置零值解析是把符号引用替换为直接引用初始化才是真正执行类变量的赋值语句和静态代码块。双亲委派模型的意思是当一个类加载器收到类加载请求时它不会自己尝试加载而是把这个请求委派给父类加载器每一层都是如此直到顶层启动类加载器。只有当父加载器反馈自己无法完成加载时子加载器才会尝试自己加载。这道题为什么值得深入讲因为作者想考察的不只是背诵而是安全思维。如果不是双亲委派用户自己写一个java.lang.String类一旦被加载整个JVM的安全体系就崩了。浏览器加载Applet、服务器加载Web应用都依赖这种层级隔离机制。回答时可以补充一个场景Java的SPI机制为什么需要线程上下文类加载器因为双亲委派模型下启动类加载器无法加载由应用类加载器提供的JDBC驱动实现类。这一句话就能体现出你对类加载的运用理解而不只是背概念。4. 设计题拆解微博Feed流如何扛住千万级大V的写入4.1 题目原意考察的是无标准答案的工程判断笔试的压轴设计题通常是请设计一个类似新浪微博的Feed流系统用户关注了1000个大V每个大V有1000万粉丝如何设计数据存储和推送方案保证用户刷新Feed流时延迟在1秒以内这类题没有标准答案面试官看的是你的分析思路和取舍能力。我当年的思路是先定义了问题的边界读写比极度悬殊、热点集中、数据总量大千亿级、实时性要求高。然后我会给出一个分层次的方案从最基础的推拉模型说起再逐步演进。4.2 读扩散与写扩散设计的核心矛盾Feed流系统最常见的两种方案是拉模式和推模式。拉模式读扩散用户刷新时实时去查询全部关注者的微博再按时间排序合并。优点是存储简单、只存一份数据缺点是每个刷新请求都要实时聚合上千人的微博粉丝量大了必然超时。推模式写扩散大V发布微博后系统立刻把这条微博推送到所有粉丝的收件箱里。这个方案的缺点是如果大V有1000万粉丝一条微博要写1000万条数据写入放大极其严重。正确的设计思路是推拉结合。对于一个拥有1000万粉丝的大V不需要把每条微博推给每个粉丝而是只推送给活跃用户对于普通用户则可以直接拉取。业界常说大V走拉模式普通用户走推模式但还要加一个活跃度维度在线用户用推模式保持实时性离线用户审核时用拉模式补全。4.3 缓存设计时间线分页与穿透保护Feed流读请求的缓存设计有个关键点时间线不能缓存成一个巨大的列表因为内存扛不住列表尾部也基本没有人访问。常见的做法是用ZSet缓存每个用户最近N条微博的ID时间戳作为score查询时按score逆序分页。初次加载50条下拉刷新时带上游标上一次最后一条微博的时间戳只查出比这个时间戳更早的50条。这种游标分页比传统的limit offset更高效因为offset深翻页时需要数据库扫描大量无用行。还需要考虑缓存穿透问题如果用户关注列表为空每次刷新都会请求数据库。解决方法是缓存空结果并设置短过期时间如果有人恶意构造大量不存在的用户ID请求就需要布隆过滤器把不存在的ID先过滤掉。这道题整个流程串下来考察的其实是你是否能站在系统全局做权衡。5. 编程题精讲手写代码时边界条件比算法本身更值钱5.1 单链表反转迭代和递归都要会编程题里出现单链表反转的概率极高因为代码量大、实现简单、可考边界条件。题目描述反转一个单链表要求空间复杂度O(1)。链表节点定义如下static class ListNode { int val; ListNode next; ListNode(int val) { this.val val; } }迭代法用三个指针prev、curr、next循环中先保存下一个节点再反转当前节点指针最后整体移动三个指针public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { ListNode nextTemp curr.next; curr.next prev; prev curr; curr nextTemp; } return prev; }递归法的写法更简洁但理解门槛更高public ListNode reverseListRecursive(ListNode head) { if (head null || head.next null) { return head; } ListNode newHead reverseListRecursive(head.next); head.next.next head; head.next null; return newHead; }我在实际笔试中看到很多同学迭代法能写出来但递归法会忘记head.next null这是最容易犯的错误。如果不把原头节点的next置空反转后链表还有一个环一旦遍历就会死循环。这种细节恰好是面试官想看到的地方边界处理和内存意识比会写核心算法更重要。5.2 手写快速排序partition基准值选择是灵魂快速排序在2015年的笔试卷子中出现过实现排序算法要求平均时间复杂度O(nlogn)这种开放题快速排序是最稳的选择。核心是partition函数它决定了排序的性能public void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivotIndex partition(arr, left, right); quickSort(arr, left, pivotIndex - 1); quickSort(arr, pivotIndex 1, right); } private int partition(int[] arr, int left, int right) { int pivot arr[right]; int i left; for (int j left; j right; j) { if (arr[j] pivot) { swap(arr, i, j); i; } } swap(arr, i, right); return i; } private void swap(int[] arr, int i, int j) { int temp arr[i]; arr[i] arr[j]; arr[j] temp; }这里我选择以最右侧元素为基准值用双指针扫描小于基准值的元素放到左侧。这个写法的好处是代码短、不容易出错。但如果你在笔试后面试追问如果数组完全逆序会怎样你要能答出每次partition基本不交换元素递归深度变成O(n)时间复杂度退化为O(n^2)。优化方案是随机选取基准值或者三数取中法。5.3 最大连续子序和一道暴露动态规划功底的题给定一个整数数组nums找到一个具有最大和的连续子数组返回其最大和。这道题在2015年出现的版本叫股票最大收益连续区间核心解法是用动态规划状态转移方程是dp[i] max(dp[i-1] nums[i], nums[i])。但更简练的实现是Kadane算法遍历时直接维护当前累加和和全局最大值public int maxSubArray(int[] nums) { int currentMax nums[0]; int globalMax nums[0]; for (int i 1; i nums.length; i) { currentMax Math.max(nums[i], currentMax nums[i]); globalMax Math.max(globalMax, currentMax); } return globalMax; }我讲解题技巧时会强调一句Math.max(nums[i], currentMax nums[i])这个决策本质上是如果之前的累加和对我没有好处我就从当前位置重新开始。这个思路在真实业务里也有对应某个监控指标如果长时间异常与其把历史脏数据一起累加不如从某个时间点重新计算基线。笔试考算法很多情况下考的是你在极端场景下会不会陷入惯性思维。5.4 最长回文子串从中心扩展写起边界条件分奇偶最长回文子串的原题要求输出最长回文子串的长度或内容O(n^2)的中心扩展法是笔试中最容易写对的方案。思路是遍历每个位置分别以该位置为中心向两边扩散奇数长度以及以该位置和下一个位置的中间为中心向两边扩散偶数长度记录最长结果public String longestPalindrome(String s) { if (s null || s.length() 0) { return ; } int start 0, end 0; for (int i 0; i s.length(); i) { int len1 expandAroundCenter(s, i, i); int len2 expandAroundCenter(s, i, i 1); int len Math.max(len1, len2); if (len end - start 1) { start i - (len - 1) / 2; end i len / 2; } } return s.substring(start, end 1); } private int expandAroundCenter(String s, int left, int right) { while (left 0 right s.length() s.charAt(left) s.charAt(right)) { left--; right; } return right - left - 1; }这道题的坑主要有两个。一是忘记处理空串和长度为1的字符串二是奇偶回文两种情况只算了一种。通过len1和len2分别计算再取最大值是稳妥的标准做法。如果面试问优化Manacher算法可以把复杂度优化到O(n)但笔试阶段先把中心扩展写对拿分比追着O(n)算法写错要好得多。我自己在评审代码时最怕的也是那种上来就背模板、边界条件一塌糊涂的候选人。6. 应试策略复盘哪些准备是有效的哪些是自我感动6.1 时间分配编程题永远值得先抢时间以这套两个半小时的卷子为例我建议的时间分配是选择判断题控制在30到40分钟简答和设计题控制在60分钟左右编程题至少留50分钟。编程题通常有2到3道如果其中有完全没思路的不要死磕先把能拿分的题目写完整再回头考虑剩下的。很多人吃亏在选择题上过于纠结一道不确定的HashMap题反复改答案最后编程题草草了事。我自己也有同样的教训与其在两个选项之间浪费五分钟不如做上标记直接跳过整个卷子做完后再回来复查。笔试的得分策略本质是在限定时间内最大化分数不是把所有题都做对。6.2 知识漏洞在哪里用真题倒推复习计划做完这套题如果你发现有几个模块比较薄弱对应的复习重点应该很明确了。给一份我后来带新人常用的总结清单HashMap源码至少读一遍特别是put、get、resize三个方法。JMM内存模型、volatile、synchronized的区别要能结合业务场景说清楚。JVM运行时数据区必须能手绘垃圾回收算法至少了解CMS和G1的区别。Spring IOC和AOP核心原理要理解不要只会注解。MySQL索引的数据结构为什么用B树而不是B树或红黑树。网络协议至少搞清楚一次完整的HTTP请求发生了什么。手写代码不能只看不练链表、树、排序、动态规划这四类每周都要各写两遍。6.3 这套题放到今天的价值变的是框架不变的是原理十年过去Spring Boot已经成为默认开发框架微服务、容器化、Serverless成为新的潮流机器学习平台也逐步铺开。但如果你今天拿这套2015年的题目去面试一个Java开发工程师会发现80%的考点依然有效。HashMap还是一样会问volatile还是一样会问类加载过程还是一样会问。原因很直接Java这门语言的底层运行机制没有变计算机操作系统和网络的基本原理没有变高并发场景下的资源竞争和性能瓶颈也没有变。真正过时的可能只是一些已经被替代的技术细节比如JVM的永久代、JDK 7的HashMap头插法、MySQL 5.6时代的主从复制配置。这些内容的价值不在于照搬使用而在于帮助我们理解技术方案的演进往往是为了解决旧方案的某个致命缺陷。知道为什么改比知道改成了什么更能体现一个人的技术功底。我自己在完成这套笔试题的复盘后最大的感受是面试不是考试而是向你未来的同事证明你有能力解决他们正在头疼的问题。纸面上的题目只是这个证明过程的第一步。那些靠背题刷出来的表面功夫到了实际业务里一戳就破那些真正理解原理、能写出健壮代码的人不管技术栈怎么变都会是团队里最稳定的中坚力量。如果你正在准备类似的笔试我的建议是别急着去背最新的面试八股文先把2015年这套题里每一个考点都吃透。它们就像河床上的巨石水底下始终在那里水位高低并不改变它们的重量。
返回列表