《互联网大厂Java求职者面试:核心知识大考验》

37 阅读6分钟

面试官:请简要介绍一下Java核心知识中面向对象的三大特性。

王铁牛:嗯,面向对象三大特性就是封装、继承、多态。封装就是把数据和操作数据的方法封装在一起;继承就是子类继承父类的属性和方法;多态就是同一个行为具有多个不同表现形式。

面试官:不错,回答得很准确。那请说说在多线程环境下,如何确保线程安全。

王铁牛:可以用synchronized关键字啊,还有就是用Lock接口及其实现类。

面试官:嗯,基本思路是对的。再问一个,说说你对JVM内存模型的理解。

王铁牛:JVM内存模型就是有堆、栈、方法区这些呗,堆里存对象,栈里存局部变量啥的。

第一轮结束。

面试官:接下来谈谈JUC包中的CountDownLatch。它在什么场景下会被使用?

王铁牛:这个嘛,就是多个线程等一个线程完成一些操作后再一起执行呗。

面试官:那CyclicBarrier呢?和CountDownLatch有什么区别?

王铁牛:CyclicBarrier是多个线程互相等待,直到都到达某个点再一起执行,和CountDownLatch不一样。

面试官:说说线程池的几种创建方式。

王铁牛:有newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool这些。

第二轮结束。

面试官:讲讲HashMap的底层数据结构和扩容机制。

王铁牛:底层是数组加链表,扩容就是数组容量不够了就翻倍扩容。

面试官:那在多线程环境下,HashMap会出现什么问题?

王铁牛:会出现链表成环,导致死循环。

面试官:如何解决HashMap在多线程环境下的问题?

王铁牛:可以用ConcurrentHashMap啊。

第三轮结束。

面试官:今天的面试就到这里,回去等通知吧。

答案:

  1. 面向对象三大特性
    • 封装:将数据和操作数据的方法捆绑在一起,对外提供统一的访问接口。这样可以隐藏内部实现细节,提高代码的安全性和可维护性。例如,一个类中的私有成员变量,通过公有的getter和setter方法来访问和修改。
    • 继承:子类继承父类的属性和方法。子类可以复用父类的代码,实现代码的重用。比如,一个父类有一些通用的方法,子类继承后可以直接使用这些方法,还可以根据自身需求进行扩展。
    • 多态:同一个行为具有多个不同表现形式。可以通过方法重写和接口实现来实现多态。比如,定义一个父类类型的变量,它可以指向子类的对象,调用子类重写后的方法时会表现出不同的行为。
  2. 多线程环境下确保线程安全
    • synchronized关键字:可以修饰方法或代码块。当一个线程访问被synchronized修饰的方法或代码块时,其他线程需要等待该线程执行完毕才能访问。例如,在一个类中有一个被synchronized修饰的方法,多个线程同时调用这个方法时,同一时间只有一个线程能进入该方法执行。
    • Lock接口及其实现类:如ReentrantLock。Lock提供了比synchronized更灵活的锁控制。可以通过lock()方法手动获取锁,unlock()方法手动释放锁,还可以实现公平锁等。比如,在一些复杂的业务场景中,需要更精确地控制锁的获取和释放时机,就可以使用Lock。
  3. JVM内存模型
    • :存储对象实例。是JVM中最大的一块内存区域,被所有线程共享。对象的创建、销毁都在堆中进行。
    • :每个线程都有自己独立的栈。栈中存储局部变量、方法调用等信息。方法执行时,会在栈中创建一个栈帧,用于存储该方法的局部变量和操作数等。
    • 方法区:存储类信息、常量、静态变量等。被所有线程共享。例如,类的字节码文件加载后就存放在方法区。
  4. CountDownLatch
    • 使用场景:适用于一个或多个线程等待其他线程完成一组操作之后再执行的场景。比如,主线程等待多个子线程完成各自的任务后,再进行汇总操作。
    • 它通过一个计数器来实现,初始化时设置一个计数值,当调用countDown()方法时计数器减1,当计数器为0时,等待的线程会被唤醒继续执行。
  5. CyclicBarrier
    • 与CountDownLatch的区别:CountDownLatch是一次性的,计数器减到0后不能再重置;而CyclicBarrier可以重用,当所有线程到达屏障点后,计数器会重置,可继续下一轮的使用。
    • 例如,在一个多线程任务中,多个线程需要共同完成一些阶段的任务,每个阶段完成后所有线程等待,等所有线程都到达这个等待点后,再一起进入下一个阶段,这时就适合用CyclicBarrier。
  6. 线程池的创建方式
    • newFixedThreadPool:创建一个固定大小的线程池,线程池中的线程数量始终保持不变。当有新任务提交时,如果线程池中有空闲线程,则立即执行;如果没有空闲线程,则任务会被放入队列中等待。适用于需要控制线程数量,避免过多线程消耗资源的场景。
    • newSingleThreadExecutor:创建一个单线程的线程池,只有一个线程在执行任务。任务会按照提交的顺序依次执行,适用于需要顺序执行任务的场景。
    • newCachedThreadPool:创建一个可缓存的线程池,如果线程池中有空闲线程,则复用空闲线程来执行新任务;如果没有空闲线程,则创建新线程来执行任务。当线程池中的线程在60秒内没有被使用时,会被销毁。适用于任务执行时间短,且任务数量较多的场景。
  7. HashMap的底层数据结构和扩容机制
    • 底层数据结构:由数组和链表组成。数组的每个元素是一个链表的头节点。当一个键值对插入时,会根据键的哈希值计算在数组中的位置,如果该位置为空,则直接插入;如果不为空,则会在链表中添加新节点。
    • 扩容机制:当HashMap中的元素个数超过数组容量 * 加载因子(默认0.75)时,就会进行扩容。扩容时,会创建一个新的更大的数组,将原数组中的元素重新计算哈希值后放入新数组中。
  8. HashMap在多线程环境下的问题及解决方法
    • 问题:在多线程环境下,当多个线程同时对HashMap进行put操作时,可能会导致链表成环,进而在后续获取元素时出现死循环。
    • 解决方法:可以使用ConcurrentHashMap。ConcurrentHashMap采用了分段锁机制,将整个哈希表分成多个段,每个段都有自己的锁。在多线程操作时,不同段的数据可以同时被访问,提高了并发性能。而且它在扩容时也采用了更高效的算法,避免了链表成环的问题。