互联网大厂Java面试:核心知识大考验
面试官:请简要介绍一下Java核心知识在实际项目中的重要性。
王铁牛:Java核心知识很重要啊,像面向对象编程这些,能让代码结构清晰,可维护性强。在项目里,不同模块可以通过类来划分,功能明确。
面试官:那说说接口和抽象类的区别,以及在什么场景下该如何选择。
王铁牛:接口是一种特殊的抽象类型,它只有方法定义没有实现。抽象类可以有抽象方法也可以有具体实现。在需要实现多继承的时候适合用接口,当有一些通用的属性和方法时用抽象类。
面试官:讲得不错。接下来问几个关于JUC的问题。在高并发场景下,如何使用CountDownLatch来协调多个线程的执行顺序?
王铁牛:CountDownLatch可以设置一个初始值,线程调用countDown方法时,初始值减一,当减到0时,所有等待的线程就可以继续执行了。比如在一个任务需要多个子任务都完成后再执行的场景下就很有用。
第一轮结束
面试官:进入第二轮,谈谈JVM的内存模型,以及垃圾回收机制是如何工作的。
王铁牛:JVM内存模型包括堆、栈、方法区等。垃圾回收机制通过标记清除、标记整理、复制算法等来回收不再使用的内存。
面试官:那如何优化JVM的性能,减少垃圾回收的频率呢?
王铁牛:可以通过调整堆大小,合理设置新生代、老年代的比例,还有避免创建过多的对象。
面试官:多线程编程中,如何避免死锁的发生?
王铁牛:要避免死锁,得保证线程获取锁的顺序一致,还可以设置锁的超时时间,避免一直等待。
第二轮结束
面试官:最后一轮,说说线程池的工作原理,以及如何合理配置线程池的参数。
王铁牛:线程池有核心线程数、最大线程数等参数。工作原理就是提交任务后,如果核心线程数没满就由核心线程执行,满了就放到队列里,队列满了再看最大线程数,若最大线程数也满了就根据拒绝策略处理。配置参数得根据任务的类型和数量来,比如计算密集型任务核心线程数可以多些,IO密集型任务队列可以大些。
面试官:HashMap在多线程环境下可能会出现什么问题,如何解决?
王铁牛:可能会出现链表成环、数据丢失等问题。解决办法可以用ConcurrentHashMap,它是线程安全的。
面试官:好的,今天的面试就到这里,回去等通知吧。
面试结束总结:本次面试围绕Java核心知识、JUC、JVM、多线程、线程池、HashMap等多方面展开。王铁牛对于一些简单问题回答得较为清晰,得到了面试官的肯定。但在面对复杂问题时,回答得不够精准和全面。例如在JVM性能优化方面,只是简单提及了一些常见方法,没有深入阐述原理和具体实践场景。在线程池参数配置上,虽然说出了大致依据,但对于更细致的考量因素没有进一步展开。整体表现有亮点也有不足,最终结果还需等待通知。
问题答案:
- 接口和抽象类的区别及应用场景:
- 接口是一种特殊的抽象类型,它只有方法定义没有实现。抽象类可以有抽象方法也可以有具体实现。
- 应用场景:当需要实现多继承的时候适合用接口,因为一个类可以实现多个接口。当有一些通用的属性和方法,子类有部分共性实现,部分需要子类各自实现时用抽象类。比如在图形绘制系统中,不同图形都有绘制方法,这可以定义在接口中,而图形的一些通用属性如颜色、位置等可以放在抽象类中。
- CountDownLatch在高并发场景下协调线程执行顺序:
- CountDownLatch可以设置一个初始值,线程调用countDown方法时,初始值减一,当减到0时,所有等待的线程就可以继续执行了。
- 例如在一个任务需要多个子任务都完成后再执行的场景下就很有用。假设一个数据分析任务,需要多个数据采集线程采集完数据后,主分析线程才能开始工作。可以给CountDownLatch设置初始值为数据采集线程的数量,每个采集线程完成任务后调用countDown,主分析线程调用await等待,当CountDownLatch的值变为0时,主分析线程就可以开始处理数据了。
- JVM内存模型及垃圾回收机制工作原理:
- JVM内存模型包括堆、栈、方法区等。堆是对象存储的地方,栈用于存储局部变量和方法调用栈帧,方法区存储类信息、常量等。
- 垃圾回收机制通过标记清除、标记整理、复制算法等来回收不再使用的内存。标记清除算法是先标记出要回收的对象,然后统一清除;标记整理算法是标记后将存活对象移动到一端,然后清理另一端;复制算法是将内存分为两块,每次只使用一块,当这块满了,将存活对象复制到另一块,然后清理原来的那块。比如新生代一般采用复制算法,老年代采用标记清除或标记整理算法。
- 优化JVM性能减少垃圾回收频率:
- 可以通过调整堆大小,合理设置新生代、老年代的比例。如果是计算密集型应用,适当增大堆大小,增加新生代中Survivor区比例,减少对象进入老年代的频率,从而减少老年代的垃圾回收压力。
- 避免创建过多的对象,尽量使用对象池技术,比如数据库连接池、线程池等,复用对象而不是频繁创建和销毁。
- 多线程编程避免死锁发生:
- 要避免死锁,得保证线程获取锁的顺序一致。比如线程A和线程B都需要获取锁L1和L2,如果总是按照相同的顺序获取锁,比如A先获取L1再获取L2,B先获取L2再获取L1,就不会出现死锁。
- 还可以设置锁的超时时间,避免一直等待。例如使用Lock.tryLock方法,尝试获取锁,如果在指定时间内获取不到就放弃,避免死锁。
- 线程池工作原理及合理配置参数:
- 线程池有核心线程数、最大线程数、队列、拒绝策略等参数。工作原理就是提交任务后,如果核心线程数没满就由核心线程执行,满了就放到队列里,队列满了再看最大线程数,若最大线程数也满了就根据拒绝策略处理。
- 配置参数时,计算密集型任务核心线程数可以多些,因为需要更多的CPU资源并行计算,比如可以设置为CPU核心数 + 1。IO密集型任务队列可以大些,因为这类任务等待IO的时间长,线程大部分时间处于等待状态,核心线程数可以少些,比如设置为CPU核心数的一半左右。最大线程数要根据系统资源和任务负载来定,避免过度创建线程导致系统资源耗尽。拒绝策略可以根据业务需求选择,比如AbortPolicy直接抛出异常,CallerRunsPolicy由调用线程处理,DiscardPolicy直接丢弃任务,DiscardOldestPolicy丢弃队列中最老的任务。
- HashMap在多线程环境下问题及解决办法:
- 在多线程环境下,HashMap可能会出现链表成环、数据丢失等问题。当多个线程同时对HashMap进行扩容操作时,可能会导致链表成环,进而影响后续的查询和插入操作。
- 解决办法可以用ConcurrentHashMap,它是线程安全的。ConcurrentHashMap采用分段锁机制,将数据分成多个段,每个段有自己的锁,这样在多线程操作时,不同段的数据可以同时被操作,提高了并发性能。