中通 JAVA软件开发 一面 面经
1、介绍一下你的实习项目经历
我在上一家公司实习期间主要负责物流管理系统的开发工作。这个系统主要处理订单流转、仓储管理、配送调度等核心业务,日均处理订单量在20万左右。
我主要参与了三个模块的开发:1)订单管理模块,负责订单的创建、状态流转、异常处理等功能,用到了状态机模式来管理订单的生命周期;2)库存管理模块,实现了库存的实时扣减和预占功能,通过Redis分布式锁保证并发安全,用消息队列做异步库存同步;3)数据报表模块,基于ElasticSearch做订单数据的聚合分析,生成各种维度的统计报表。
在这个过程中遇到过一些技术挑战,比如高峰期接口响应慢的问题,我通过添加Redis缓存、优化SQL查询、引入本地缓存等方式,把接口响应时间从500ms优化到了80ms左右。还遇到过分布式事务的问题,订单创建成功但库存扣减失败,我引入了本地消息表+定时补偿的方案来保证最终一致性。
这段实习经历让我对分布式系统有了更深的理解,也积累了解决实际问题的经验。
2、HashMap和ConcurrentHashMap在设计上有什么区别
这两个Map在设计上的核心区别是线程安全性和并发性能:
- 线程安全方面,HashMap是非线程安全的,多线程环境下可能出现数据丢失、死循环等问题。ConcurrentHashMap是线程安全的,采用了分段锁和CAS机制来保证并发安全。
- 数据结构方面,HashMap是数组+链表+红黑树的结构。ConcurrentHashMap在JDK7用的是Segment分段锁,每个Segment是一个小的HashMap;JDK8放弃了Segment,改用Node数组+链表+红黑树,锁的粒度更细化到每个数组位置。
- 并发控制方面,ConcurrentHashMap的put操作,如果数组位置为空就用CAS插入,如果有冲突就用synchronized锁住链表或红黑树的头节点。这样不同位置的操作可以并发执行,大大提升了并发度。
- 扩容机制方面,HashMap扩容时会阻塞所有操作。ConcurrentHashMap支持多线程协助扩容,正在扩容时其他线程可以帮忙迁移数据,提升扩容效率。
- size统计方面,HashMap直接返回size变量。ConcurrentHashMap用baseCount加上counterCells数组来统计,减少了并发竞争。
实际使用中,单线程或明确不会并发访问用HashMap性能更好,多线程环境必须用ConcurrentHashMap保证安全。
3、线程池的工作原理是什么,核心线程空闲时是什么状态
线程池的工作原理可以分几个层次来说:
- 核心参数包括corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime空闲时间、workQueue任务队列、threadFactory线程工厂、handler拒绝策略。
- 任务提交流程是:首先判断核心线程数是否已满,没满就创建新线程执行任务;核心线程满了就把任务放入队列;队列满了就创建非核心线程;线程数达到最大值就执行拒绝策略。
- 线程复用机制是核心线程执行完任务后不会销毁,而是从队列中取新任务继续执行。具体实现是通过Worker对象的runWorker方法,里面有个while循环不断从队列取任务,取到就执行,取不到就阻塞等待。
关于核心线程空闲时的状态:
- 核心线程在没有任务时会调用workQueue.take()或poll()方法从队列获取任务,这时线程处于WAITING或TIMED_WAITING状态,不会占用CPU资源。
- 如果用的是LinkedBlockingQueue的take()方法,线程会一直阻塞等待,状态是WAITING。如果配置了allowCoreThreadTimeOut为true,会用poll(keepAliveTime)方法,超时后线程会被回收,状态是TIMED_WAITING。
- 默认情况下核心线程不会被回收,会一直保持在线程池中等待任务。非核心线程超过keepAliveTime没有任务就会被销毁。
我在项目中用线程池处理异步任务时,会根据业务特点设置合理的参数,比如IO密集型任务核心线程数设置为CPU核心数的2倍,CPU密集型设置为CPU核心数+1。
4、JVM有哪些核心参数和常用的垃圾回收器
JVM的核心参数主要分几类:
- 堆内存参数,-Xms设置初始堆大小,-Xmx设置最大堆大小,一般设置成一样避免动态扩容。-Xmn设置新生代大小,-XX:SurvivorRatio设置Eden和Survivor的比例,默认8:1:1。-XX:MaxMetaspaceSize设置元空间大小。
- GC相关参数,-XX:+UseG1GC指定使用G1收集器,-XX:MaxGCPauseMillis设置最大停顿时间,-XX:G1HeapRegionSize设置Region大小。-XX:+PrintGCDetails打印GC日志,-Xloggc指定日志文件路径。
- 其他参数,-Xss设置栈大小,默认1MB。-XX:+HeapDumpOnOutOfMemoryError在OOM时自动dump堆,-XX:HeapDumpPath指定dump文件路径。
常用的垃圾回收器有:
- Serial收集器,单线程收集,适合客户端应用或小内存场景,STW时间短但吞吐量低。
- Parallel收集器,多线程并行收集,注重吞吐量,适合后台计算任务。JDK8默认使用。
- CMS收集器,以最短停顿时间为目标,采用标记-清除算法,适合对响应时间要求高的应用。缺点是会产生内存碎片,CPU占用高。
- G1收集器,JDK9之后的默认收集器,把堆分成多个Region,可以预测停顿时间。适合大堆内存场景,兼顾吞吐量和停顿时间。我们生产环境用的就是G1。
- ZGC收集器,JDK11引入的低延迟收集器,停顿时间不超过10ms,适合超大堆内存和对延迟极其敏感的场景。
实际使用中要根据应用特点选择,一般推荐G1,它的表现比较均衡。
5、Stream流中map和flatMap有什么区别
map和flatMap都是Stream的中间操作,但处理方式不同:
- map是一对一映射,对流中每个元素应用函数,返回一个新元素。比如把List<String>转成List<Integer>,每个字符串映射成它的长度。返回的是Stream<T>。
- flatMap是一对多映射,对每个元素应用函数后返回一个流,然后把所有流合并成一个流。比如把List<List<String>>拍平成List<String>。返回的也是Stream<T>,但会把嵌套的流展开。
- 使用场景上,map用于简单的类型转换或字段提取。flatMap用于处理嵌套结构,比如一个订单包含多个商品,要获取所有商品列表就用flatMap。
- 举个例子,有个List<String>包含多个句子,要获取所有单词。用map会得到Stream<String[]>,每个句子split后是个数组。用flatMap配合A
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经 文章被收录于专栏
Java面试圣经,带你练透java圣经