十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从Java基础到并发编程,面试复习重点清单

从Java基础到并发编程,面试复习重点清单 把Java面试题拆开揉碎了看无非是在检验一件事你是否真的理解计算机底层在你写的每一行代码背后做了什么。面试官问“String和StringBuilder的区别”不是听你背“不可变、可变”而是想听到字符串常量池、char数组的复制、拼接时的性能损耗这些背后逻辑。真正的复习不是刷几十个题而是一条主线贯穿下去从基础语法到JVM内存模型再到并发编程其实都在追问同一件事——你写下的代码运行时究竟怎么被处理形态各异的“基础”——从语法到思想被问到面向对象三大特性时几乎人人都能说出“封装、继承、多态”但紧接着的追问往往挂掉一半人“重写和重载有什么本质区别”只会背答案的人说“方法名相同参数不同或子类覆盖了父类的方法”而真正理解的人会回答——重写是运行时多态的体现重载在编译期就已盖上印章。重写发生在运行时只有到了执行那一刻才根据实际对象类型确定调用谁它的底层是虚方法表分派。再往下问“为什么构造方法不能重写”因为构造方法在字节码层面对应invokespecial指令不参与虚方法分派何况它连返回值类型都没有谈不上“覆盖”。基础面试考的就是这种“语法背后的原因”。Object类也是绕不开的暗礁。equals和hashCode的契约几乎每场都考不重写hashCode的equals是一场事故当对象放入HashMap时哈希桶定位依赖hashCodeequals用来在同桶内查找真正匹配的key。如果两个equals相等的对象hashCode不一致你永远找不到“对方”。更隐蔽的是写“覆写hashCode”时不要用随机值也不要直接返回对象内存地址而必须保证相等对象有相同哈希。这背后不只是技术细节更是你用实体对象做key时无法重现数据的血泪教训。另外好多人分不清深拷贝与浅拷贝的底层差异其实一句话就能看清深拷贝是复制出独立的值浅拷贝复制的是通往值的路。集合框架——数据结构与工程妥协的集散地语法基础揭开后集合就变成迎面而来的硬菜而HashMap拥有无可撼动的地位。HashMap的底细就是你对哈希表和红黑树的掌握程度。要能说清数组链表红黑树为何存在为什么链表长度达到8且数组容量达到64才树化——一方面哈希碰撞符合泊松分布链表长度到8时概率已经极低另一方面树化是为了将最坏情况从O(n)降为O(logn)。还要知道扩容时的resize机制旧数组里的节点要么原位不动要么移动到“原索引旧容量”的新位置因为每次扩容都是翻倍节点是否迁移取决于扩容新增的那一位是0还是1。很多面试者背得出“JDK 8因为头插法会产生环形链表所以改为尾插”却答不出更深的道理并发下的HashMap没有安全可言它只保证单线程内的语义。改成尾插只是让链表复制不再制造死循环但多线程同时put时数据丢失、size统计错乱依然存在。因此不要问“HashMap线程不安全是不是bug”它设计之初就是单线程工具硬要用在多线程环境下那是你的选择问题。线程安全的Map请找ConcurrentHashMap它用CAS加synchronized锁桶把竞争粒度压到极小的范围。ArrayList和LinkedList的对比是另一个经典陷阱。许多人只答“数组连续内存、链表不连续”听起来没错却没有说出真正影响性能的是内存局部性与CPU缓存预取。ArrayList的扩容背后是数组复制而LinkedList的每次get(index)都要从头遍历所以在绝大多数真实业务场景里ArrayList完胜。LinkedList唯一有优势的场景是已知能在中间插入删除、且插入点已经通过迭代器定位到的时候。至于for-each循环里删除元素会抛ConcurrentModificationException要知道那只是迭代器维护的modCount在报警ModCount不是安全机制它只是bug的报警器告诉你结构被意外修改了。真要多线程遍历修改要选弱一致性的CopyOnWriteArrayList或ConcurrentHashMap作为容器它们放弃了对结构修改的“实时一致性”换来了更宽松的并发遍历。JVM——把内存摆上台面集合再往下挖一层就撞见JVM。面试官问“堆、栈和方法区有什么区别”不要背完定义就停。Java程序员不会谈JVM就像司机不懂发动机一样危险。栈管程序如何执行每个线程有自己的栈里面有局部变量表、操作数栈、动态链接、返回地址堆管对象实例是垃圾回收的主战场方法区存类型元数据、静态变量与常量池。到了Java 8永久代被元空间取代不再占用堆内存而是使用直接内存——原因正是给字符串常量池等更灵活的空间也避免了永久代OOM。还要想明白一条映射链局部变量在线程栈上是私有的所以基本类型不存在跨线程共享的问题对象实例在堆上天然共享所以它的成员变量成为并发访问的源头。垃圾回收机制要有自己的一套推理。GC不是做完一次就完事而是“分代”的艺术。新生代对象朝生夕死适合复制算法用Eden和两个Survivor分区周转老年代对象存活率高不适合复制所以改用标记清除或标记整理。判断对象存活的核心算法是可达性分析从GC Roots出发能找到的才算“活着”。这里常被追问“哪些对象能当GC Roots”答案是栈帧中局部变量引用的对象、静态变量引用的对象、被native方法引用的对象、JNI引用等。紧接着类加载就要出场——双亲委派不是规矩是保护它防止用户篡改JDK类库。当你试图定义java.lang.String并加载时AppClassLoader会一路向上委派Bootstrap拒绝加载非系统的“java.”开头的用户类并抛出SecurityException。Tomcat为什么能同时布多个不同版本的同名类因为它自己实现了WebAppClassLoader在特定范围打破了双亲委派。但记住打破双亲委派是特定场景的妥协不是日常炫技的理由。并发编程——终极试金石走到并发编程这里很多候选人的差距就彻底拉开了。并发三特性——可见性、有序性、原子性是所有并发知识的第一性原理。synchronized靠锁同时保证三者volatile只能保证可见性和有序性却无法保证复合操作的原子性。volatile不是锁它只是让你“看见”别人改了什么——CPU层面的缓存一致性协议会让被修饰的变量更新穿透到主内存还会禁止相关指令重排列。但“看见”和“抢到”是两码事你看见i变成5另一线程也看见i是5然后同时执行i1最后写回仅加了一次。这就是为什么用volatile操作计数器是典型的并发错误。那么什么时候该用volatile状态标志位、单例模式里的double-checked locking这些写入后立刻被其他线程读取、且不依赖旧值的场景才合适。说到锁JVM对synchronized的优化值得细品。偏向锁假设只有一个线程进房间于是不设真正锁只在对象头里打标记一旦竞争变多升级为轻量级锁用CAS自旋等待自旋过头才升级为重量级锁挂起线程。这套升级路径说明JVM在拼命地减少锁阻塞。而java.util.concurrent包下的Lock其强大源自AQS。Lock的精髓在于AQS里那个state状态的争夺与等待。ReentrantLock每次lock/unlock都会加减一个state值用它记录可重入次数CAS成功就持有锁失败则进入FIFO等待队列。只有懂得state、等待队列、条件变量之间的协作流程才算是真懂Lock也才能解释公平锁和非公平锁的实现差异——非公平锁一上来先抢一次state抢不到才乖乖排队。线程池作为压轴题很少有人真讲透。七大参数背下来并不难难的是理解任务提交的完整路径线程数低于corePoolSize就新建核心线程否则任务插入工作队列等待队列满后继续把线程数提升到maximumPoolSize如果队列仍然满且线程数已达上限才触发拒绝策略。线程池不是越多越好队列满了就拒绝的机制才是精髓因为这保证你在背压之下不会无限制地消耗系统资源。不少人喜欢死记“CPU密集N1、IO密集2N”的公式但这只是僵化经验真实世界还要看任务是否阻塞在外部依赖、锁竞争程度、GC频率等。如果追问ThreadLocal你得马上意识到它在ThreadLocalMap中用的是弱引用key可一旦线程存活时间长比如线程池里的常驻线程value就会一直沿着强引用链无法回收。合适的做法是每次用完调用remove而不是依赖弱引用或手动置空来完成清理。回顾这一整条路线基础语法、集合框架、JVM、并发编程并不是四座孤岛而是层层递进的因果链。你在某个环节脱口而出的“为什么”往往能在另一个模块中找到更本质的答案。与其焦虑地背诵一长串清单不如问问自己是否愿意沿着一个“为什么”深挖下去。从基础到并发是一条完整链路没有一蹴而就的“清单”只有持续挖深“为什么”的习惯。当你能把这些概念串成一个能自洽解释“代码如何运行”的故事面试官问什么其实已经不重要了。
返回列表