面试官:Java多线程和JUC你懂吗?谢飞机:我飞过!——互联网大厂技术面试搞笑实录(一)
场景:某互联网大厂会议室,阳光明媚但气氛紧张。面试官面无表情地翻着简历,对面坐着一位自称“精通全栈”的求职者——谢飞机。
第一轮提问:Java 多线程基础
面试官:谢飞机是吧?我看你简历写了“熟悉Java多线程编程”,那你先说说,Thread和Runnable有什么区别?
谢飞机:这个我知道!Thread是类,Runnable是接口!而且 Java 不支持多继承,所以用Runnable更好!
面试官(微微点头):不错,理解到位。那你说说,为什么推荐使用线程池而不是手动创建线程?
谢飞机:因为……new Thread() 太费内存了!就像买手机,不能每次打电话都买个新手机吧?得租!线程池就是租赁公司!
面试官(嘴角微扬):比喻有点意思。那你知道ExecutorService怎么关闭吗?
谢飞机:当然!调用shutdown()就行!它会等任务执行完再关。还有个shutdownNow(),那是直接拔电源!
面试官:还行。那如果我想让主线程等待所有子线程完成后再继续,怎么做?
谢飞机:用join()啊!或者CountDownLatch,我还会用CyclicBarrier呢!
面试官:嗯,基本功可以。
第二轮提问:JUC 并发工具进阶
面试官:既然提到CountDownLatch,说说它和CyclicBarrier的区别?
谢飞机:呃……都是倒数计数器?一个是从10减到0,一个是反过来加?
面试官:……
谢飞机:哦不对!CountDownLatch是一个线程等其他多个线程完成;CyclicBarrier是多个线程互相等,大家一起出发,像百米赛跑!
面试官:勉强及格。那Semaphore呢?用来做什么?
谢飞机:信号灯!控制并发数量的!比如停车场有10个车位,Semaphore(10),进来一辆车 acquire(),出去 release()!
面试官:例子不错。那ReentrantLock和synchronized有啥区别?
谢飞机:都能加锁!但ReentrantLock更高级,可以尝试锁、可中断、还能指定公平锁!
面试官:那它是怎么实现可重入的?
谢飞机:呃……内部有个计数器?谁持有锁就记名字?具体我忘了,反正能重入!
面试官(皱眉):行吧……
第三轮提问:线程安全与实战场景
面试官:现在有一个高并发场景,多个线程同时对一个共享变量进行累加操作,你会怎么处理?
谢飞机:用volatile!
面试官:volatile能保证原子性吗?
谢飞机:呃……能?不能?好像不能……那用synchronized块包起来!
面试官:还有别的办法吗?
谢飞机:用AtomicInteger!CAS机制!无锁并发!
面试官:那 CAS 有什么缺点?
谢飞机:ABA问题!还有自旋太耗CPU!
面试官:怎么解决 ABA?
谢飞机:呃……加版本号!AtomicStampedReference!
面试官:不错。最后一个问题:线程池的核心参数有哪些?
谢飞机:corePoolSize、maximumPoolSize、workQueue、keepAliveTime、threadFactory、handler……
面试官:如果队列满了且线程数达到最大,会发生什么?
谢飞机:看拒绝策略!默认是 AbortPolicy,抛异常!还可以自己定义!
面试官:好,今天的面试就到这里。你的基础还行,有些地方需要加强。回去等通知吧。
谢飞机(起身鞠躬):谢谢面试官!我回去就把 JUC 源码打印出来当被子盖!
参考答案详解
1. Thread 和 Runnable 的区别
Thread是类,Runnable是接口。- Java 单继承限制下,实现
Runnable更灵活。 - 实际上
Thread类也实现了Runnable接口。 - 推荐使用
Runnable或Callable配合线程池使用。
2. 为什么使用线程池?
- 避免频繁创建/销毁线程带来的资源消耗。
- 控制并发数量,防止资源耗尽。
- 提供统一的管理机制(监控、统计、拒绝策略等)。
3. ExecutorService 关闭方式
shutdown():温和关闭,不再接收新任务,等待已提交任务执行完毕。shutdownNow():立即关闭,尝试中断正在运行的线程,返回未执行的任务列表。
4. 主线程等待子线程
thread.join():适用于少量线程。CountDownLatch:适合一个或多个线程等待其他多个线程完成。CyclicBarrier:多个线程相互等待,达到屏障点后一起继续。Phaser:更灵活的同步工具,支持动态注册。
5. CountDownLatch vs CyclicBarrier
| 对比项 | CountDownLatch | CyclicBarrier | |--------|----------------|---------------| | 计数方向 | 向下减少 | 达到数量即触发 | | 是否可重用 | 不可重用 | 可重用(reset) | | 使用场景 | 主线程等待多个任务完成 | 多个线程互相等待 | | 实现机制 | 计数为0唤醒 | 达到阈值触发 |
6. Semaphore 作用
- 用于控制同时访问特定资源的线程数量。
- 常用于限流、资源池(如数据库连接池)。
acquire()获取许可,release()释放许可。
7. ReentrantLock vs synchronized
| 特性 | synchronized | ReentrantLock | |------|--------------|---------------| | 语法级别 | JVM 层面(关键字) | API 层面(代码调用) | | 可中断 | 否 | 是(lockInterruptibly) | | 超时获取 | 否 | 是(tryLock(timeout)) | | 公平性 | 非公平 | 可设置公平锁 | | 条件变量 | wait/notify | Condition |
8. ReentrantLock 实现可重入原理
- 内部使用
AQS(AbstractQueuedSynchronizer)实现。 - 维护一个 state 变量表示锁状态,thread 记录持有锁的线程。
- 同一线程重复获取锁时,state++,释放时 state--,直到为0才真正释放。
9. 共享变量并发累加解决方案
synchronized同步块ReentrantLock显式锁AtomicInteger等原子类(基于 CAS + volatile)- LongAdder(高并发下性能更好)
10. CAS 缺点
- ABA问题:值从A→B→A,CAS 无法察觉中间变化。解决方案:
AtomicStampedReference加版本号。 - 自旋开销:循环重试可能导致 CPU 空转。可通过
Thread.yield()优化。 - 只能保证单个变量的原子性:多变量需使用锁或其他机制。
11. 线程池核心参数
corePoolSize:核心线程数maximumPoolSize:最大线程数keepAliveTime:非核心线程空闲存活时间unit:时间单位workQueue:任务队列threadFactory:线程工厂handler:拒绝策略
12. 拒绝策略
AbortPolicy:抛出RejectedExecutionExceptionCallerRunsPolicy:由提交任务的线程执行DiscardPolicy:静默丢弃DiscardOldestPolicy:丢弃队列中最老的任务,重试提交