
1. 高并发面试的核心考察点解析在Java高并发领域的面试中面试官通常会从三个维度进行考察基础理论、实战经验和系统设计能力。我参加过数十场技术面试也作为面试官考核过不少候选人发现很多人在准备时容易陷入死记硬背的误区。实际上高并发面试更像是一次技术对话重点在于展示你的思考过程。1.1 必问的底层原理JUCjava.util.concurrent包是面试中的重灾区但90%的候选人只停留在API使用层面。面试官最想听到的是你对这些工具类底层实现的见解。比如问到ConcurrentHashMap时可以这样展开在JDK8中ConcurrentHashMap放弃了分段锁的设计改用Node数组链表/红黑树的结构。putVal方法通过CASsynchronized实现线程安全这里有个精妙的设计——只锁住当前操作的链表头节点tabAt方法获取。sizeCtl这个变量控制着扩容时机当某个线程检测到需要扩容时会通过transfer方法完成数据迁移...1.2 高频出现的场景题如何设计一个秒杀系统这类问题几乎成了必考题。回答时要注意分层阐述前端层静态化按钮防抖随机丢弃请求网关层限流令牌桶/漏桶黑名单服务层缓存预热异步扣减库存数据层乐观锁库存分段我曾用RedisLua实现过一个分布式扣减方案关键点在于将库存分成多个slot用INCRBY和DECRBY保证原子性配合WATCH实现简单的事务控制。实测QPS能达到2万以上。1.3 容易忽略的性能陷阱很多候选人能说出synchronized和ReentrantLock的区别但被问到为什么HashMap的get方法在不同并发情况下性能差异巨大时就懵了。这里涉及到CPU缓存行和伪共享问题// 伪共享的典型例子 class FalseSharing { volatile long x; // 与y可能在同一缓存行 volatile long y; }解决方案是使用Contended注解JDK8或手动填充class Padding { long p1,p2,p3,p4,p5,p6,p7; // 前置填充 volatile long value; long p9,p10,p11,p12,p13,p14,p15; // 后置填充 }2. 线程池的深度拷问2.1 参数设置的黄金法则线程池的七个参数corePoolSize、maximumPoolSize...怎么配置我总结了一个三步法根据任务类型确定核心线程数CPU密集型核数1IO密集型核数*1平均等待时间/平均计算时间队列容量根据业务容忍度决定瞬时高峰SynchronousQueue直接移交平滑流量ArrayBlockingQueue固定缓冲拒绝策略的选择日志记录CallerRunsPolicy快速失败AbortPolicy关键点使用ThreadPoolExecutor的beforeExecute和afterExecute方法添加监控逻辑记录任务执行耗时。2.2 线上问题排查实录去年我们遇到过一个线程池死锁问题主线程提交任务后等待Future.get()而子任务又在等待主线程释放某个锁。通过jstack抓取线程快照后发现main #1 prio5 waiting on condition java.lang.Thread.State: WAITING at sun.misc.Unsafe.park(Native Method) - parking to wait for 0x000000076bf622b8 at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:425) pool-1-thread-1 #2 prio5 waiting for monitor entry java.lang.Thread.State: BLOCKED at com.example.Service.method(Service.java:123) - waiting to lock 0x000000076bf622c0解决方案是改用CompletableFuture的thenApplyAsync让回调在另一个线程执行。3. 锁优化的实战技巧3.1 从synchronized到AQS理解锁升级过程很重要但面试时更应该展示对锁粒度的把控能力。比如这段代码// 粗粒度锁 public synchronized void transfer(Account from, Account to, int amount){ from.debit(amount); to.credit(amount); } // 细粒度锁 public void transfer(Account from, Account to, int amount){ synchronized(from){ synchronized(to){ from.debit(amount); to.credit(amount); } } }后者虽然提高了并发度但可能引发死锁。我的经验是使用System.identityHashCode确定锁获取顺序设置超时机制的tryLock对于账户类场景可以考虑锁分段3.2 读写锁的妙用在配置中心这类读多写少的场景中ReentrantReadWriteLock比CopyOnWriteArrayList更合适。我做过一个压测对比实现方式读QPS写QPS内存占用synchronized12,000800低CopyOnWrite28,000120高ReadWriteLock85,0001,500中关键技巧在于使用StampedLock的乐观读模式public Object read() { long stamp lock.tryOptimisticRead(); Object data loadData(); if(!lock.validate(stamp)){ stamp lock.readLock(); try { data loadData(); } finally { lock.unlockRead(stamp); } } return data; }4. 并发容器的选型策略4.1 ConcurrentHashMap的注意事项虽然CHM是线程安全的但组合操作仍需额外保护。比如这段代码就有问题// 错误用法 if(!map.containsKey(key)){ map.put(key, value); }应该改用computeIfAbsentmap.computeIfAbsent(key, k - createExpensiveValue(k));我在实际使用中还发现一个坑JDK8的CHM在扩容时如果同时进行查询操作可能导致数据暂时不可见。解决方案是对于关键查询使用fullScan模式V safeGet(K key) { V value; do { value map.get(key); } while (value null map.size() 0); return value; }4.2 阻塞队列的对比分析不同阻塞队列在10万次操作下的性能对比单位ms队列类型offerpoll适用场景ArrayBlockingQueue128115固定容量任务调度LinkedBlockingQueue153142无界队列注意OOMPriorityBlockingQueue210198优先级任务SynchronousQueue8579直接传递线程池常用特别提醒DelayQueue的实现依赖优先级队列其take操作会循环检查队首元素可能造成CPU空转。建议设置合理的等待策略。5. 原子类的隐藏知识点5.1 CAS的ABA问题解决方案经典的ABA问题可以通过版本号解决但AtomicStampedReference的实现有些性能损耗。我在高频交易场景中测试发现// 传统方式 AtomicStampedReferenceInteger ref new AtomicStampedReference(0, 0); ref.compareAndSet(0, 1, stamp, stamp1); // 优化方案 AtomicMarkableReferenceInteger ref new AtomicMarkableReference(0, false); ref.compareAndSet(0, 1, false, true);后者在百万次操作下能节省约30%的时间适合对版本连续性要求不高的场景。5.2 LongAdder的适用场景当发现AtomicLong成为瓶颈时比如统计接口调用次数LongAdder是更好的选择。它的秘密在于分散热点------------------ Cell[] | cell0| cell1| cell2| // 每个线程操作不同cell ------------------ \ | / \ | / baseValue // 最终结果base∑cell实测在100个线程并发时LongAdder的吞吐量是AtomicLong的6倍。但要注意调用sum()法时会触发全量汇总开销较大适合高频更新、低频读取的场景6. 并发编程的避坑指南6.1 线程安全的错误认知我曾见过这样的线程安全代码// 伪线程安全 class Counter { private final ListAtomicInteger counts Collections.synchronizedList(new ArrayList()); void add() { counts.add(new AtomicInteger(0)); // 这里的add是线程安全的 counts.get(counts.size()-1).incrementAndGet(); // 但这两步组合不是原子操作 } }正确的做法是使用ConcurrentHashMap配合compute方法class SafeCounter { private final ConcurrentHashMapInteger, AtomicInteger map new ConcurrentHashMap(); void increment(int key) { map.compute(key, (k, v) - { if (v null) return new AtomicInteger(1); v.incrementAndGet(); return v; }); } }6.2 内存可见性的陷阱即使使用volatile也不能保证复合操作的原子性class VolatileExample { volatile int x 0; void unsafeIncrement() { x; // 实际上是read-modify-write三步操作 } }解决方案使用AtomicInteger如果性能敏感可以考虑sun.misc.Contended避免伪共享对于复杂对象仍然需要synchronized或显式锁7. 分布式并发的延伸考察7.1 从单机锁到分布式锁当面试官问如何实现分布式锁时不要急于回答Redis的SETNX。完整的考量应该包括互斥性RedLock算法争议较大容错性Zookeeper的临时顺序节点性能ETCD的lease机制自研方案基于数据库的乐观锁版本号我最近实现的一个方案结合了Redis和本地缓存boolean tryLock(String key, long expireTime) { // 先检查本地缓存 if (localCache.get(key) ! null) return false; // Redis原子操作 String result redisTemplate.execute(script, Collections.singletonList(key), UUID.randomUUID().toString(), String.valueOf(expireTime)); if (OK.equals(result)) { localCache.put(key, true); // 双检锁降低Redis压力 return true; } return false; }7.2 并发设计的模式选择根据业务特点选择不同模式模式实现方式适用场景吞吐量领导者追随者ThreadPoolExecutor短平快任务高工作窃取ForkJoinPool递归分解任务中反应器Netty EventLoopIO密集型极高流水线Disruptor RingBuffer数据流水线处理超高在日志处理系统中我测试过Disruptor和BlockingQueue的对比百万条日志处理时间从1200ms降到350msGC次数从15次减少到2次CPU利用率从60%提升到85%关键配置参数DisruptorLogEvent disruptor new Disruptor( LogEvent::new, 1024, // ringBuffer大小必须是2的幂 DaemonThreadFactory.INSTANCE, ProducerType.MULTI, // 多生产者模式 new BlockingWaitStrategy() // 平衡性能和CPU消耗 );8. 性能调优的实战案例8.1 上下文切换的成本通过vmstat发现系统的cscontext switch指标过高procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 6 0 0 250000 10240 306000 0 0 115 42 1050 8500 30 25 45 0 0优化手段减少线程数从200降到50使用协程Quasar fiber修改锁策略偏向锁-自旋锁调整后cs降到1200吞吐量提升40%。8.2 伪共享问题的定位使用perf工具检测缓存命中率perf stat -e cache-references,cache-misses java MyApp发现某个AtomicLong的缓存命中率只有65%通过填充解决后提升到98%。关键技巧是使用JOLJava Object Layout工具查看对象布局System.out.println(ClassLayout.parseInstance(counter).toPrintable());输出示例OFFSET SIZE TYPE DESCRIPTION 0 4 (object header) # Mark Word 4 4 (object header) # Klass Pointer 8 4 int AtomicLong.value # 实际数据 12 4 (alignment gap) # 这里需要填充9. JVM层面的并发优化9.1 锁粗化与锁消除JIT编译器会做这些优化但有时需要手动干预。比如这段代码void method() { synchronized(this) { /* 操作1 */ } synchronized(this) { /* 操作2 */ } synchronized(this) { /* 操作3 */ } }可以优化为void method() { synchronized(this) { /* 操作1 */ /* 操作2 */ /* 操作3 */ } }但要注意过大的同步块会导致竞争加剧。我的经验法则是同步块执行时间不超过1ms。9.2 线程栈的合理配置通过-Xss设置栈大小需要权衡太小StackOverflowError默认1MB太大内存浪费每个线程独占建议计算密集型256KB足够深度递归2MB一般应用512KB查看实际使用情况jstack pid | grep -oE prio[0-9] tid[0-9xA-Fa-f] | wc -l10. 面试中的高频算法题10.1 手写生产者消费者不要用BlockingQueue取巧面试官想考察的是wait/notify的掌握class ManualBlockingQueueT { private final QueueT queue new LinkedList(); private final int capacity; public synchronized void put(T item) throws InterruptedException { while(queue.size() capacity) { wait(); } queue.add(item); notifyAll(); } public synchronized T take() throws InterruptedException { while(queue.isEmpty()) { wait(); } T item queue.remove(); notifyAll(); return item; } }进阶问题为什么用while而不是if因为可能存在虚假唤醒spurious wakeup。10.2 实现一个可重入锁考察对AQS的理解class MyReentrantLock { private static class Sync extends AbstractQueuedSynchronizer { protected boolean tryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if(c 0) { if(compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if(current getExclusiveOwnerThread()) { setState(c acquires); return true; } return false; } protected boolean tryRelease(int releases) { if(Thread.currentThread() ! getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); int nextc getState() - releases; boolean free nextc 0; if(free) setExclusiveOwnerThread(null); setState(nextc); return free; } } private final Sync sync new Sync(); public void lock() { sync.acquire(1); } public void unlock() { sync.release(1); } }11. 并发工具的高级用法11.1 CompletableFuture的组合技巧处理多个异步任务时这些方法特别有用// 任一完成即继续 CompletableFuture.anyOf(future1, future2) .thenAccept(result - {...}); // 全部完成后处理 CompletableFuture.allOf(future1, future2) .thenRun(() - { Object r1 future1.join(); Object r2 future2.join(); ... }); // 超时控制 future.orTimeout(1, TimeUnit.SECONDS) .exceptionally(ex - {...});我在订单系统中用这种模式将串行调用改成了并行CompletableFutureInventory inventoryFuture CompletableFuture.supplyAsync(() - checkInventory(sku)); CompletableFuturePrice priceFuture CompletableFuture.supplyAsync(() - getPrice(sku)); inventoryFuture.thenCombine(priceFuture, (inv, price) - { return createOrder(inv, price); }).exceptionally(ex - { log.error(Create order failed, ex); return null; });11.2 ForkJoinPool的实战优化对于计算密集型任务合理设置并行度很重要// 默认并行度Runtime.getRuntime().availableProcessors() ForkJoinPool pool new ForkJoinPool(32); long result pool.invoke(new RecursiveTaskLong() { protected Long compute() { if(problemSize threshold) { return sequentialSolve(); } else { RecursiveTask left new Task(...); RecursiveTask right new Task(...); left.fork(); right.fork(); return left.join() right.join(); } } });经验值数组/集合处理阈值10,000~100,000树形结构阈值100~1,000个节点矩阵运算阈值256x25612. 并发测试的必备技能12.1 JMH基准测试正确使用JMH的姿势BenchmarkMode(Mode.Throughput) OutputTimeUnit(TimeUnit.MILLISECONDS) State(Scope.Thread) public class LockBenchmark { private Lock lock new ReentrantLock(); private int counter; Benchmark public void testLock() { lock.lock(); try { counter; } finally { lock.unlock(); } } Benchmark public void testSynchronized() { synchronized(this) { counter; } } }运行参数建议java -jar benchmarks.jar -f 2 -wi 5 -i 5 -t 4-f 迭代次数-wi 预热迭代-i 测量迭代-t 线程数12.2 并发bug重现技巧使用jcstress工具测试内存可见性JCStressTest Outcome(id 0, 0, expect Expect.ACCEPTABLE_INTERESTING) State public class ConcurrencyTest { int x, y; Actor public void actor1() { x 1; y 1; } Actor public void actor2(II_Result r) { r.r1 y; r.r2 x; } }常见模式指令重排序会出现(0,1)或(1,0)可见性问题会出现(0,0)原子性问题会出现中间状态13. 项目经验的有效表达13.1 STAR法则的应用描述并发相关项目时Situation系统原有QPS 500峰值时出现库存超卖Task设计一个支持5000 QPS的秒杀方案Action采用RedisLua本地缓存库存分段Result峰值QPS达到6200无超卖现象重点突出你做的技术决策和权衡比如 在Redis和ZK之间选择Redis因为我们的场景允许少量超额最终一致性而ZK的写性能无法满足要求13.2 性能数据的准备准备这些关键指标优化前后QPS对比系统资源使用率CPU/内存99线、95线延迟GC次数和耗时示例表达 通过将synchronized改为ReentrantLock配合合适的自旋次数接口99线从120ms降到45msGC时间减少60%14. 系统设计的加分项14.1 容错设计展示你对故障的预见性降级策略当Redis不可用时切换本地缓存熔断机制Hystrix配置超时时间补偿事务定时任务修复不一致数据14.2 监控体系完善的监控应该包括线程池活跃度监控锁等待时间监控队列积压报警死锁检测我常用的监控项ThreadPoolExecutor executor new ThreadPoolExecutor(...); executor.setRejectedExecutionHandler((r, e) - { metrics.increment(rejected.tasks); throw new RejectedExecutionException(); }); // 通过JMX暴露关键指标 ManagementFactory.getPlatformMBeanServer() .registerMBean(new ThreadPoolMXBean(executor), new ObjectName(metrics:typeThreadPool));15. 面试后的思考延伸当面试官问还有什么问题时可以探讨贵司如何处理分布式事务的并发控制在高并发场景下日志收集方案如何选择对于Java虚拟线程Project Loom的看法这既能展示你的深度思考也能了解团队的技术水平。我曾在一个面试中和CTO讨论了半小时的RCURead-Copy-Update在Java中的实现可能性最终拿到了offer。