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

资讯详情

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

2018货拉拉秋招Java笔试题解析:集合、并发与JVM核心考点

2018货拉拉秋招Java笔试题解析:集合、并发与JVM核心考点 这份2018年货拉拉秋招Java笔试题放在今天来看依然有很强的参考价值。虽然时隔几年但Java核心基础知识点的考察逻辑没有变依然是集合、并发、JVM、算法那几座大山。我花了一整晚把卷二A这套题完整做了一遍结合当年的题目回忆和现在的面试环境把涉及到的知识点、答题思路、以及其中容易踩的坑一并整理出来。1. 从货拉拉的业务场景反推笔试考点这份卷子到底在考什么先说结论货拉拉作为一家同城货运平台核心业务是订单匹配、司机调度、路径规划和实时计费这套系统对高并发、低延迟、数据一致性的要求极高。所以这份笔试题卷二A的侧重点非常明显——Java基础功底 并发编程 集合框架底层 JVM内存模型 基础算法几乎没有偏题怪题全是实打实的核心知识点。如果你是准备面试的Java工程师我建议先别急着刷题而是先理解货拉拉这类互联网公司为什么要考这些内容。订单调度系统里成千上万的司机和用户同时在线每一个订单的创建、抢单、取消、支付都会触发大量的并发请求这就直接对应了试卷里关于HashMap并发问题、synchronized和ReentrantLock区别、线程池参数设置的考点。而路径规划、里程计算这些功能又对算法和数据结构有硬性要求链表反转、排序算法这些基础题就是为了筛掉那些只会CRUD、没有底层功底的候选人。这份卷子还有一个特点考察面广但深度适中。它不像大厂那种动辄让你手写红黑树实现的魔鬼难度而是更倾向于考察“你是否真正理解并能解释清楚核心机制”。比如同样考HashMap它不会让你实现一个HashMap而是问JDK 1.7和1.8的结构差异、扩容时机、为什么线程不安全这些都是实际开发中一定会遇到的细节。所以准备这份卷子重点不是死记硬背答案而是把每个知识点背后“为什么这样设计”的逻辑搞清楚。2. 基础送分题里的陷阱Object类方法、字符串比较与自动装箱2.1 equals和hashCode为什么重写equals必须重写hashCode这套卷子第一道大题基本就是关于Object类方法的考察最核心的就是equals和hashCode的关系。很多人在这一题上丢分不是因为不知道要重写而是说不清楚为什么要一起重写。关键是理解HashMap和HashSet的存储逻辑。当你往HashSet里放对象时集合先通过hashCode()计算哈希值定位到桶然后再通过equals()比较桶内元素是否相同。如果只重写equals而不重写hashCode会导致两个逻辑上相等的对象哈希值不同被分到不同的桶里Set就无法去重了。反过来如果只重写hashCode而不重写equals虽然定位到了同一个桶但equals比较结果不相等同样无法判断重复。我当时的答题思路是分三步先说明二者的契约关系相等对象必须具有相等的哈希码再说明HashSet/HashMap的实际工作流程最后用一个反例说明只重写一个会引发什么问题。这样答既完整又显得有逻辑比单纯背结论要靠谱得多。2.2 String、StringBuilder、StringBuffer不只是问可变不可变字符串相关的题目几乎是Java笔试必考但很多人只记住了“String不可变StringBuilder线程不安全StringBuffer线程安全”这个结论却不知道面试官真正想听什么。这道题的深入答法是从JVM内存角度分析。String用final修饰字符数组每次拼接都会创建新对象如果是在循环里拼接字符串会产生大量中间对象触发频繁GC性能极差。StringBuilder的append方法直接修改内部的char数组不会产生中间对象所以循环拼接时性能最好。StringBuffer的每个方法都加了synchronized线程安全但带来了额外的锁竞争开销在不需要线程安全的场景下性能反而更差。另外还有一个隐藏考点字符串常量池。String a hello;和String b new String(hello);的区别以及intern()方法的作用这些在笔试里也经常出现。答题时如果能把常量池和堆内存的关系说清楚分数会明显不一样。2.3 自动装箱与缓存池Integer的比较坑了多少人自动装箱这题特别经典Integer a 127; Integer b 127;比较结果是true但换成128就变false了。原因是Integer内部有一个IntegerCache缓存池缓存了-128到127之间的所有Integer对象在这个范围内的自动装箱直接返回缓存对象超出范围就新建对象。这个机制放在货拉拉的业务场景里特别有代入感。比如司机端的计价模块如果你用Integer来缓存每公里的单价而且单价恰好都在127以内那没问题一旦单价超过127用比较就会出现难以察觉的bug。正确做法是一律用equals()比较包装类型或者直接用基本类型int。还有一道容易错的题Integer i 1; i 1;很多人以为这行代码报错其实它内部是先拆箱成int相加再自动装箱赋值回Integer这个过程完全合法。这些细节题目不复杂但考察的是你对语言机制的理解深度不是背结论就能答好的。3. 集合框架底层原理HashMap、ArrayList与ConcurrentHashMap的深度剖析3.1 HashMap的实现细节从数组加链表到红黑树的演进货拉拉这套卷子对HashMap的考察相当细几乎把能问的点都问了一遍底层数据结构是什么、JDK 1.8相比1.7做了哪些优化、默认负载因子为什么是0.75、扩容机制是怎样的、为什么线程不安全。先说结构差异。JDK 1.7是数组加链表新元素采用头插法JDK 1.8改为数组加链表加红黑树链表长度超过8且数组长度大于64时转为红黑树新元素采用尾插法。这个改动解决了两个问题一是链表过长时查询效率从O(n)降到O(logn)二是1.7的头插法在并发扩容时会产生环形链表导致get操作死循环。负载因子0.75是个时间和空间的折中方案。负载因子越小数组越稀疏冲突越少但浪费空间越大则冲突越多影响查询效率。0.75是经过数学计算和工程实践验证的经验值HashMap在扩容时容量翻倍保证容量始终是2的幂次方这样(n-1) hash就能高效替代取模运算。关于线程不安全除了JDK 1.7的环形链表问题1.8还有一个数据覆盖问题两个线程同时put如果哈希值相同且当前位置为null两个线程都会进入if判断并创建新节点后写入的会覆盖先写入的数据。这也是为什么在高并发的订单缓存场景下绝对不能直接用HashMap。3.2 ArrayList和LinkedList看似简单实则处处是坑ArrayList和LinkedList的区别这题如果只答“一个基于数组一个基于链表”基本拿不到满分。卷子里的问法更刁钻在什么场景下LinkedList的插入性能反而比ArrayList差这个问题的关键在于是否知道LinkedList的插入操作有个隐藏的遍历过程。add(int index, E element)方法需要先从头开始遍历找到index位置的节点这个遍历是O(n)的。如果index接近列表尾部遍历成本更高。而ArrayList的插入虽然要移动元素但System.arraycopy是native方法底层用的是内存复制移动大量元素时效率并不低。所以如果插入点在中部LinkedList未必比ArrayList快。还有一个ArrayList的经典坑subList()返回的是内部类的视图不是复制出来的新列表。对subList做结构性修改会影响原列表而且如果subList的parent列表被修改过再操作subList会抛ConcurrentModificationException。3.3 ConcurrentHashMap的锁粒度进化从分段锁到CAS加synchronizedConcurrentHashMap是并发集合里最重要的考点。JDK 1.7版本采用分段锁设计把数据分成一段一段的每段一个锁最多支持16个线程并发写。JDK 1.8放弃了分段锁改用CAS加synchronized锁住链表或红黑树的头节点锁粒度从“段”细化为“单个桶”。答题时需要说明每个版本锁粒度的变化和性能影响。1.8的实现在并发度上更高两个线程只要操作的不是同一个桶就不会互相阻塞。而且1.8的synchronized锁的是链表的头节点锁竞争时利用synchronized在JDK 1.6之后引入的锁升级机制偏向锁、轻量级锁、重量级锁在低竞争场景下性能比ReentrantLock更好。同时注意size()方法的实现1.8的ConcurrentHashMapsize()是通过sumCount方法累加baseCount和CounterCell数组的值这里也有一个不能保证绝对实时的问题笔试中偶尔会考到多线程下如何统计元素个数这个点值得留意。4. JVM内存模型与垃圾回收这道题直接决定你能不能进下一轮4.1 运行时数据区划分堆、栈、方法区的边界与共享关系JVM相关的题目在任何一家公司的Java笔试里都是重头戏。这套卷子考了运行时数据区的划分要求画出内存模型并说明哪些是线程共享的、哪些是线程私有的。线程私有的有虚拟机栈、本地方法栈、程序计数器。线程共享的有堆、方法区JDK 1.8之后叫元空间。答题时我习惯用“一个类从加载到运行经历了什么”来串联这些区域类文件通过类加载器加载到方法区/元空间类的静态变量和常量池信息存这里创建对象时对象实例和数组分配在堆上方法调用时每个方法创建栈帧压入虚拟机栈栈帧里有局部变量表、操作数栈、动态链接、方法出口。有一个容易混淆的点需要留意JDK 1.7之后字符串常量池从方法区移到了堆中而JDK 1.8又用元空间替换了永久代。这个演进过程如果能在答题时讲清楚会让面试官觉得你确实关注了JVM的发展变化不是只在背面试题。4.2 垃圾回收算法与收集器不只是背名字要说明适用场景垃圾回收部分的题目主要考察可达性分析算法、三种基本回收算法标记-清除、标记-复制、标记-整理以及常见收集器Serial、Parallel、CMS、G1的特点。答题时最重要的是区分对象存活判定和回收算法之间的关系。可达性分析从GC Roots出发沿着引用链搜索不可达的对象会被标记为可回收。GC Roots包括虚拟机栈中引用的对象、方法区中静态属性引用的对象、常量引用的对象、JNI中引用的对象。每种回收算法都有明显的优缺点需要结合实际场景说明。标记-清除会产生内存碎片适合老年代这种对象存活率高的区域标记-复制把内存分为两块每次只用一块GC时把存活对象复制到另一块再整体清理实现简单但浪费空间适合新生代这种对象朝生夕死的区域标记-整理则把存活对象向一端移动解决碎片问题适合老年代。这题的加分项是提到G1收集器的Region化设计。G1把堆划分为多个大小相等的Region每个Region都可以扮演Eden、Survivor或者Old区通过维护一个优先列表跟踪每个Region的回收价值和回收时间优先回收价值最大的Region。这种设计让G1可以在大堆场景下做到可预测的停顿时间。4.3 内存溢出排查思路从OutOfMemoryError到实际定位热词里出现了java: outofmemoryerror说明这个考点相当热门。这套卷子里有一道场景题线上系统频繁Full GC某个接口响应时间飙升给出排查思路。我的回答思路分四步。第一步用jstat -gcutil pid 1000观察GC情况确认老年代是否持续增长Full GC频率是否过高。第二步用jmap -dump:live,formatb,fileheap.dump pid导出堆快照然后通过MAT分析看哪些对象占用的内存最大。第三步结合业务代码判断是否有大对象频繁创建或者是否有集合类对象不断添加数据没有被清理。第四步如果确认是内存泄漏而不是内存不足用jstack抓线程快照排查是否有线程阻塞导致的对象无法释放。还要区分几种常见的OutOfMemoryError堆溢出Java heap space栈溢出StackOverflowError元空间溢出Metaspace以及创建线程时无法分配内存导致的Unable to create new native thread。每种溢出的排查方向都不同堆溢出用jmap分析对象线程溢出检查是不是创建了太多线程或者线程栈设置的过大。5. 多线程与并发货拉拉笔试的重头戏也是拉开差距的地方5.1 线程生命周期与状态转换别答漏了BLOCKED和WAITING的区别多线程题目在卷二A中占了比较大的比重。第一道基础题是线程的六种状态以及状态之间的转换条件这题看似简单但很多人会把BLOCKED和WAITING搞混。区别在于进入BLOCKED状态是因为线程在竞争synchronized锁时没有获得锁它处于等待另一个线程释放锁的状态而进入WAITING状态是因为调用了wait()、join()或LockSupport.park()方法它是在主动等待某个条件满足。BLOCKED是“被动等待获取锁”WAITING是“主动等待被唤醒”。还有一个细节sleep()方法不会释放锁也不改变持有锁的状态线程从RUNNABLE进入TIMED_WAITING等时间到了自动回到RUNNABLE。而wait()会释放锁释放之后其他线程才能进入同步代码块。这个区别在生产者消费者模型的题目里特别关键。5.2 synchronized与ReentrantLock的底层差异从Monitor到AQSsynchronized和ReentrantLock的区别是并发编程里必须掌握的。这套卷子考到了锁的底层实现需要说明synchronized是基于JVM层面的Monitor锁实现的而ReentrantLock是基于JDK层面的AQSAbstractQueuedSynchronizer实现。JDK 1.6之后synchronized引入了锁升级机制偏向锁、轻量级锁、重量级锁。偏向锁会记录第一个获取锁的线程ID后续该线程再次进入同步块时无需CAS如果出现锁竞争升级为轻量级锁通过CAS自旋获取锁自旋一定次数后还是没获取到升级为重量级锁线程进入阻塞状态。ReentrantLock的优势在于更灵活支持公平锁和非公平锁切换支持超时获取锁tryLock(timeout, unit)支持多个条件变量Condition精确唤醒指定线程。这里的底层AQS核心是volatile int状态变量加CLH队列FIFO双向队列获取锁时通过CAS修改状态变量失败则把当前线程封装成Node节点放入队列尾部自旋等待。我当年在答题时把这题往深了写把AQS的模板方法模式也提了一下比如tryAcquire/tryRelease是子类实现的钩子方法ReentrantLock内部通过继承AQS的Sync类来定义自己的同步逻辑。这虽然不是必考点但写出来确实能给阅卷人留下好印象。5.3 线程池的核心参数与拒绝策略七参数一个都不能错线程池参数设置的题目货拉拉的卷子直接给了一个场景订单处理系统峰值时并发线程数约200每个任务平均耗时100ms问核心线程数怎么设置、队列怎么选择。核心答案围绕ThreadPoolExecutor的七个参数核心线程数、最大线程数、空闲存活时间、存活时间单位、工作队列、线程工厂、拒绝策略。核心线程数的计算公式是CPU核心数 / (1 - 阻塞系数)或者更实用地按任务每秒请求数 线程数 * (1000ms / 任务耗时)反推。以场景为例如果目标QPS是2000每个任务100ms那么线程数 2000 * 0.1 200个。队列选择上IO密集型任务用SynchronousQueue或LinkedBlockingQueue因为IO操作会阻塞线程需要用更大的线程池来处理更多并发CPU密集型任务用ArrayBlockingQueue控制队列长度避免任务积压太多。拒绝策略有四种AbortPolicy直接抛异常、CallerRunsPolicy调用者执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃最老的任务。实际生产环境一般用CallerRunsPolicy靠调用者线程执行任务来自然限流不会丢弃订单。5.4 volatile与可见性为什么双重检查锁要加volatile这道题结合了单例模式和Java内存模型考察点很细。单例模式的双重检查锁写法中instance变量为什么必须用volatile修饰答案是为了防止指令重排序。instance new Singleton();这行代码在JVM层面会被拆成三步分配内存、初始化对象、把引用赋值给变量。如果这三步被重排序为1→3→2另一个线程在第二步完成前判断instance不为null直接返回了一个尚未初始化完成的对象程序就乱了。volatile在JDK 1.5之后提供了happens-before保证禁止对volatile变量的读写操作进行重排序同时保证写操作对后续读操作可见。顺带一提内存模型的原子性、可见性、有序性问题也是常考内容。synchronized和Lock保证原子性和可见性volatile保证可见性和有序性但不保证原子性。i这种操作在并发下线程不安全就是因为它是“读取-计算-写入”三个步骤的复合操作volatile解决不了这个问题必须用AtomicInteger的CAS操作来保证原子性。6. 算法题速写手写排序与链表操作的实战技巧6.1 冒泡排序的优化避免写错边界条件和提前退出逻辑算法部分是卷二的必考内容排序算法的考察重点集中在冒泡排序和快速排序。冒泡排序看起来简单但笔试中经常有人因为边界条件写错导致排序结果不对。标准的冒泡排序是两层循环外层控制轮数内层比较相邻元素并交换。一个常见的优化是增加一个标志位如果某一轮遍历中没有任何交换发生说明数组已经有序提前结束外层循环。6.2 快速排序的partition写法挖坑法和双指针法任选一快速排序的考察频率比冒泡更高因为它涉及递归、分治、复杂度分析等多个知识点。手写快排时关键是partition函数的写法常见的有两种。挖坑法的思路是选定一个基准值先从右往左找比基准小的元素填坑再从左往右找比基准大的元素填坑最后把基准放入最终位置。双指针法是左右两个指针同时遍历交换不符合条件的元素。两种方式都能通过测试但需要注意边界条件左指针和右指针相遇时循环结束循环条件while (i j)内部还要加i j的判断防止数组越界。回答快排复杂度时需要区分最好情况和最坏情况平均O(nlogn)最坏O(n²)。最坏情况发生在数组已经有序每次选基准都选到最大或最小元素导致递归树退化成链表。优化思路是随机选取基准值或者用三数取中法避免最坏情况的出现。6.3 链表反转与合并迭代法和递归法都要会链表题几乎是Java笔试的标配卷二考了链表反转和有序链表合并。链表反转的迭代思路是用prev指针指向前一个节点curr指针指向当前节点每次循环把curr.next指向prev然后prev和curr同时后移一位直到curr为空。边界条件需要注意链表为空或只有一个节点的情况。有序链表合并的迭代解法是创建一个哑节点dummy作为新链表的头部用一个tail指针维护当前链表的最后一个节点每次比较两个链表的头节点把较小的节点接入tail后面直到有一条链表为空再把另一条链表剩余部分接上去。手写这些算法题时有一个面试官特别看重的习惯就是先考虑边界条件再写主逻辑。如果能把空链表、单个节点、两个链表长度不同的情况都考虑到代码的鲁棒性就体现出来了这在算法部分会有明显加分。7. 框架、数据库与设计模式实际开发能力的软考察7.1 Spring IoC和AOP回答要结合源码来谈虽然框架内容的考察比重没有基础大但Spring的IoC控制反转和AOP面向切面编程还是出现了。考察的目的不是看你背了多少概念而是看你能不能结合Bean生命周期和实际应用场景来说明。IoC的核心是BeanFactory和ApplicationContext的关系。BeanFactory是顶层接口定义了getBean等操作ApplicationContext继承了BeanFactory增加了国际化、事件发布、自动注册BeanPostProcessor等能力。Bean的生命周期包括实例化、属性填充、初始化、使用、销毁几个阶段中间穿插着BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization方法这些扩展点是Spring实现AOP、事务、异步等功能的基础。AOP的实现机制是基于动态代理如果目标类实现了接口JDK动态代理生成一个接口实现类如果没实现接口CGLIB通过继承目标类生成子类。AOP的应用场景包括事务管理、日志记录、权限校验、性能监控。答题时把这些结合具体场景说清楚比空洞地背“面向切面编程”的定义强得多。7.2 MySQL索引与事务最左前缀原则和隔离级别是必答题数据库在Java笔试中是必考的内容重点是索引和事务。索引部分最常考的是最左前缀原则联合索引(a, b, c)在查询时如果条件包含a、a和b、a和b和c可以走索引但只包含b或只包含b和c时无法使用该索引。查询优化器会先按最左边的列排序然后才按后续列排序所以跳过第一列索引就失效了。还有索引失效的场景对索引列使用函数WHERE DATE(create_time) 2024-01-01、隐式类型转换WHERE phone 13800138000phone是varchar类型、LIKE以通配符开头WHERE name LIKE %张。这些在实际开发中很容易踩坑。事务隔离级别这题四种级别读未提交、读已提交、可重复读、串行化要能说出来同时要说明各自能解决什么问题脏读、不可重复读、幻读。MySQL默认是可重复读并且在可重复读级别下通过MVCC多版本并发控制解决了快照读的幻读问题但当前读SELECT FOR UPDATE等加锁读的幻读还需要通过间隙锁next-key lock来防止。7.3 设计模式在业务场景中的落地单例、工厂、策略设计模式部分不会单独考定义而是结合场景让你选择合适的设计模式。比如订单有多种支付方式微信、支付宝、银行卡如何设计正确答案是策略模式定义一个支付接口每种支付方式实现一个策略类通过策略工厂根据支付类型获取对应的处理类这样新增支付方式时无需修改核心代码符合开闭原则。工厂模式和抽象工厂模式的区别也是考点工厂模式是一个工厂类根据参数创建不同类型的产品抽象工厂模式是创建一个产品族的多个对象。答题时最好能结合生活中的例子说明比如工厂模式就像一家餐厅根据用户点菜制作不同菜品抽象工厂模式就像一家连锁餐厅总部为不同地区的分店提供不同口味的一整套菜品。单例模式里除了双重检查锁静态内部类和枚举方式也可以实现单例。静态内部类利用了类加载机制保证线程安全枚举方式则天然的防止反射攻击和序列化破坏。这些实现方式多掌握一个面试时就能多一分灵活性。8. 从这份卷子看Java面试的备考方向哪些知识点值得花时间深挖整套卷二A做下来我有一个明显的感受它考察的是“你平时有没有真正思考过这些机制背后的设计逻辑”而不是“你背了多少面试题”。比如HashMap它不会直接问源码里某一行做了什么而是问为什么负载因子是0.75为什么JDK 1.8用尾插法替换头插法这需要你站在设计者的角度去理解问题。如果你正在准备类似的Java笔试我建议按以下优先级分配精力集合框架源码HashMap、ConcurrentHashMap和JVM垃圾回收是最高优先级因为几乎所有公司都会考并发编程synchronized、ReentrantLock、线程池、volatile是拉开差距的关键算法题需要保持手感每天至少手写一道排序或链表题Spring和MySQL是加分项答得好会让面试官觉得你有实际项目经验。还有一个很重要的点笔试答题时不要只写结论要把推导过程写出来。比如问线程池参数除了给出数值还要写出你是怎么算出来的——任务耗时、目标QPS、CPU核心数之间的关系这个思考过程比最终答案更重要。另外提醒一点做笔试题时注意时间分配。卷二A的题量不算小既有选择题又有简答题还有手写代码题如果前面的基础题耗时太多后面的算法题可能会写不完。我的策略是遇到不会的先跳过先把有把握的题做完再回头思考难题这样能最大化得分。准备时务必自己先动手做一遍题目在限时环境下模拟测试总结答题节奏。
返回列表