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

资讯详情

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

从集合到并发:Java面试重点专题整理

从集合到并发:Java面试重点专题整理 Java面试官最爱问的第一个问题往往不是你懂多少并发原理而是你手里的集合到底安不安全。ArrayList、HashMap、HashSet这些从大学课本里走出来的老朋友在单线程环境下乖巧温顺一旦丢进多线程的漩涡立刻变成定时炸弹。面试的真正分水岭不是你能不能背出“线程安全”和“线程不安全”的标签而是你能不能说清楚为什么HashMap在并发put时可能让CPU飙升到100%为什么ConcurrentHashMap的锁粒度要一再细化从集合到并发这是Java工程师绕不过去的天梯也是面试中最能拉开差距的战场。集合的“不安全”不是缺点而是设计选择很多人把ArrayList和HashMap的线程不安全视为缺陷这其实是一种误读。JDK的设计哲学是“默认不保证线程安全由使用者在需要时自行加锁”。如果所有集合类都内置沉重的同步机制单线程性能将全面崩盘。线程安全从来不是免费的午餐而是用性能换来的保险。面试时你若能点破这一层比单纯背结论高明得多。拿HashMap来说它的并发问题不是“偶尔出错”而是可能引发致命的结构性损坏。JDK 7中头插法在扩容时会产生环形链表下一次get操作就死循环。JDK 8改成了尾插法环形链表问题消失了但put时多个线程同时执行size依然会导致size不准甚至覆盖掉刚刚插入的值。HashMap的并发风险本质上是“数据结构在写入途中被破坏”的风险而不是简单的数据不一致。面试官问到这个层面你就该抛出“fail-fast机制”了。fail-fast机制一种“及时止损”的傲慢迭代器遍历集合时如果其他线程修改了集合会抛出ConcurrentModificationException。这个“快速失败”听起来很酷但它其实是一种无奈下的“鸵鸟策略”我无法保证你安全就干脆让你崩溃得明明白白。面试中常问的“为什么ArrayList遍历时不能增删”答案就在这——modCount字段一旦变化迭代器立刻翻脸。但“快速失败”不保证一定发生它依赖于“运气”。如果恰好修改发生在同一时刻modCount还没来得及变迭代器可能安然无恙。这种不确定性恰恰是面试官喜欢挖的坑fail-fast不是并发控制而是“基于充分竞争假设的日志记录器”。真正想并发遍历你该用CopyOnWriteArrayList或ConcurrentHashMap的迭代器——它们都建立在“弱一致性”之上。从HashMap到ConcurrentHashMap锁的进化史HashTable把所有方法用synchronized锁住多线程读也要排队性能惨不忍睹。ConcurrentHashMap的出现直接宣告了“一股脑锁整张表”的死亡。JDK 7的ConcurrentHashMap用分段锁——把Map分成16个Segment每个Segment是一把独立的ReentrantLock不同线程操作不同Segment可并行。JDK 8则更激进直接放弃分段改用CAS synchronized锁Node。锁粒度从“一段”细化为“一个桶”并发度从固定的16变为理论上的“桶的个数”。面试时你要能解释为什么JDK 8要放弃Segment因为分段锁的并发上限受限于Segment数量一旦Segment被扩容锁的数量不便动态调整。而锁桶之后性能与扩容完全解耦。JDK 8的ConcurrentHashMap是一门“用CAS做乐观更新用synchronized做悲观的锁升级”的杂学——当某个桶的第一个节点CAS成功就不需要加锁如果CAS失败说明有竞争再锁住那个桶。这种“先试慢速路再走快速道”的策略才是它高并发性能的来源。锁升级与CAS并发原语的两副面孔CAS是无锁并发的基础。Java里AtomicInteger的incrementAndGet底层就是Unsafe的compareAndSwapInt。CAS的精髓是“读取-比较-交换”三步但CPU一条指令完成所以原子。CAS最大的软肋是ABA问题——你比对的旧值等于原值但中间可能被改过又改回。用AtomicStampedReference加版本号才能治标治本。面试官常问“ABA问题了解吗”你不仅要答出定义还得说出解决手段。而synchronized在JDK 6之后也脱胎换骨。它不再是“重量级锁”的代名词而是经历了偏向锁→轻量级锁→重量级锁的膨胀过程。无竞争时只有一次CAS记录线程ID轻度竞争时用CAS抢锁抢不到才挂起线程进入阻塞队列。锁不是越高级越好而是越适应竞争强度越好。面试中问“synchronized和ReentrantLock的区别”千万别只答“一个是关键字一个是类”。你要点出synchronized自动释放锁、不可中断、非公平ReentrantLock支持可中断获取、超时获取、多个Condition、公平锁。但本质上synchronized现在也实现了“锁升级”的优化两者性能差距已微乎其微。线程池面试里的万能砖哪里需要哪里搬线程池是并发问题的最浓缩考点。面试官问“线程池参数有哪些”其实是在问“你对资源管理有没有直觉”。corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、RejectedExecutionHandler七个参数背后是一个完整的任务提交流程。你要能说出当任务提交时先看核心线程数是否满没满则新建线程满了则入队队列满了再看最大线程数还没满则新建非核心线程满了则走拒绝策略。这里的坑是“队列的容量和最大线程数”的配合关系。如果用的是无界队列如LinkedBlockingQueue默认容量maximumPoolSize直接失效——任务永远进队列不触发拒绝策略。所以Executors.newFixedThreadPool其实是一个“隐藏的坑”它的核心线程数和最大线程数相同队列无界一旦任务堆积内存飙升。拒绝策略的选择也要讲出艺术。AbortPolicy直接抛异常适合“宁可失败也不要堆积”的场景CallerRunsPolicy让提交任务的线程自己跑天然提供背压DiscardOldestPolicy丢弃最老任务适合允许丢旧任务的系统。你在面试中若能指出“ThreadPoolExecutor的4种拒绝策略映射了4种产品哲学”立刻就能降维打击。AQS所有并发工具的灵魂JUC包里的ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier底层全是AbstractQueuedSynchronizerAQS。AQS内置一个volatile类型的state变量和一个CLH变种的等待队列。获取锁就是尝试把state从0变成1释放锁就是改回0。不同工具只是对state的“判定和修改规则”不同ReentrantLock的state是重入次数Semaphore的state是剩余许可数CountDownLatch的state是还没到达的线程数。面试时被问“AQS是如何实现公平锁的”你要答出公平锁在tryAcquire时优先检查队列里有没有前驱节点有就让位非公平锁直接CAS抢一次抢不到再进队列。公平锁的代价是线程切换频繁吞吐量更低非公平锁的代价是可能“插队”导致饥饿但好处是减少了线程唤醒的开销。这种“公平与效率的悖论”是面试官最爱听你展开的部分。并发工具类别只会背名字CountDownLatch和CyclicBarrier的差别是面试必问。CountDownLatch是一次性的主线程await等N个countDown完成计数器归零后就不能再复用。CyclicBarrier是可循环的一组线程互相等待等最后一个到位一起释放然后屏障“重置”可以再来一轮。CountDownLatch是“一人等所有人”CyclicBarrier是“所有人等彼此”。更犀利一点的说法是CountDownLatch适合“等待某个事件发生”CyclicBarrier适合“多线程分阶段协调”。Semaphore则是“流量控制”思想的浓缩。它可以同时允许多个线程进入临界区但最多不超过N个。Semaphore申请许可时如果失败线程进入队列等待这和synchronized的阻塞完全不同。你可以用它做一个简单的限流器但注意Semaphore的公平性同样可以配置。并发工具的价值不在于它们各自是什么而在于你能否在真实场景中选出正确的那一个。volatile与内存模型并发的不变基石volatile保证可见性和有序性但不保证原子性。面试官喜欢问“为什么volatile不能保证的原子性”你要答出操作是读-改-写三步volatile只保证每次读都是最新值但三步之间可能被其他线程插入读写。真正要原子递增得用AtomicInteger或synchronized。volatile最典型的应用场景是“状态标志位”——一个线程写其他线程读且写操作不依赖原值。Java内存模型JMM规定线程对变量的操作必须在工作内存进行不能直接操作主内存。volatile的读写会强制刷新工作内存和主内存的副本这就是可见性的来源。Happens-Before规则是面试的进阶考点程序次序规则、volatile变量规则、传递性规则这些不是背出来就完事要能用来解释“双重检查锁为什么需要volatile”。双重检查锁中单例对象的实例化不是一个原子操作分配内存、初始化对象、将引用指向内存。这三步可能被JIT重排序为“先指向内存再初始化”导致另一个线程拿到一个尚未构造完全的对象。volatile禁止这种重排序才让双重检查锁真正安全。如果你能把这个故事倒背如流面试官就会相信你是真的理解并发而不是背模板。死锁与活锁并发编程的暗面讲完并发工具面试官必然会转向“你如何处理死锁”。死锁的四个必要条件互斥、占有且等待、不可剥夺、循环等待。面试时你不仅要背出这四个条件还要能针对每个条件给出打破方案用tryLock超时获取避免无限等待尽量按固定顺序获取多个锁从源头消除循环等待或者用重入锁的可中断特性来响应外部中断。但更高级的回答是死锁只是最糟的一种“锁失败”活锁和饥饿同样致命。活锁是线程不断重试但始终拿不到锁表现为“礼让到死”饥饿是某些线程永远被优先级低的调度策略压制。能区分死锁、活锁、饥饿并分别给出应对策略才是并发能力的完整展现。从集合到并发你真正要建立的是“权衡思维”集合与并发不是两个孤立的主题。HashMap和ConcurrentHashMap的对比其实是在“安全”和“性能”之间做取舍synchronized和ReentrantLock的取舍是在“自动化”和“灵活性”之间做权衡线程池参数的设计是在“吞吐”和“资源耗尽”之间找平衡。面试官真正期待的是你能够不假思索地说出每种选择的代价与收益。所以当你准备面试时不要死记“HashMap线程不安全ConcurrentHashMap线程安全”这种话。你应当闭上眼睛想象一个并发put的瞬间桶位冲突、CAS失败、链表的next指针被覆盖、size计数器失真……这些画面一旦在你脑海里运转起来你就能真正理解为什么Java的并发原语长得如此复杂。并发编程没有银弹只有不断权衡后得出的“相对最优解”。能走到这一步你对“从集合到并发”的理解就超越了90%的候选人。剩下的10%要靠你在真实的高并发系统里去摔打、去复盘、去优化——因为面试可以装熟但系统不会陪你演戏。
返回列表