《互联网大厂Java求职者面试大挑战:核心知识与实战应用》

31 阅读10分钟

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

王铁牛:嗯……这个嘛,三大特性就是封装、继承、多态。封装就是把类的属性和方法包装起来,对外提供统一的接口;继承就是子类可以继承父类的属性和方法;多态就是同一个方法可以根据对象的不同类型表现出不同的行为。

面试官:回答得不错。那在多线程环境下,如何确保数据的一致性?

王铁牛:可以使用锁呀,比如synchronized关键字,它可以保证同一时间只有一个线程能访问被它修饰的代码块或方法。还有就是使用Lock接口,它提供了更灵活的锁控制。

面试官:很好。接下来问几个关于JUC的问题。CountDownLatch和CyclicBarrier有什么区别?

王铁牛:呃……这个,CountDownLatch是用来等待一组操作完成,它的计数器是递减的,当计数器为0时,等待的线程可以继续执行。CyclicBarrier是让一组线程相互等待,直到所有线程都到达某个屏障点,然后再一起继续执行,它的计数器可以循环使用。

第一轮结束,王铁牛对简单问题回答得还可以。

面试官:说说JVM的内存结构。

王铁牛:JVM内存结构主要有堆、栈、方法区、程序计数器、本地方法栈。堆是存放对象实例的地方;栈是线程私有的,存放局部变量和方法调用的上下文;方法区存储类信息、常量、静态变量等;程序计数器记录当前线程执行的字节码指令地址;本地方法栈用于执行本地方法。

面试官:那垃圾回收主要针对哪些区域进行回收?

王铁牛:主要针对堆中的对象进行回收。当对象不再被引用时,就会被垃圾回收器标记并回收。

面试官:如何判断一个对象是否可以被回收?

王铁牛:有两种方法,一种是引用计数法,就是给对象添加一个引用计数器,当引用增加时计数器加1,引用减少时计数器减1,当计数器为0时对象就可以被回收。另一种是可达性分析算法,通过一系列的GC Roots对象作为起始点,从这些节点开始向下搜索,如果一个对象到GC Roots没有任何引用链相连,那么这个对象就是不可达的,可以被回收。

第二轮结束,王铁牛回答得有些磕磕绊绊了。

面试官:讲讲多线程中线程池的工作原理。

王铁牛:线程池就是预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务,如果线程池中的线程都在忙碌,任务就会被放入队列中等待。线程池有几个重要的参数,比如核心线程数、最大线程数、队列容量等。

面试官:那如何合理设置线程池的参数?

王铁牛:呃……这个要根据具体的业务场景来定吧。如果任务执行时间短,核心线程数可以设置小一些;如果任务执行时间长,核心线程数可以设置大一些。最大线程数要考虑系统的资源情况,不能设置太大导致资源耗尽。队列容量也要根据任务量来设置。

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

王铁牛:会出现数据丢失和死循环问题。因为HashMap在扩容时可能会导致链表形成环形结构,从而导致死循环。而且在多线程同时put元素时,可能会导致数据覆盖。

面试结束,王铁牛整体表现有好有坏,简单问题回答尚可,但复杂问题回答得不够清晰准确。面试官让王铁牛回家等通知,后续会根据情况给出反馈。

答案:

  • Java面向对象编程三大特性
    • 封装:将类的属性和方法包装起来,对外提供统一接口,实现信息隐藏和数据保护。例如一个类中有私有属性和公共的访问方法,外部只能通过这些公共方法来访问和修改私有属性。
    • 继承:子类继承父类的属性和方法,实现代码复用。比如一个子类可以继承父类的一些通用方法和属性,在此基础上再添加自己特有的功能。
    • 多态:同一个方法根据对象的不同类型表现出不同行为。可以通过方法重写来实现多态,比如父类和子类有相同名称的方法,但实现不同,当使用子类对象调用该方法时,实际执行的是子类重写后的方法。
  • 多线程环境下确保数据一致性的方法
    • synchronized关键字:它可以修饰代码块或方法,保证同一时间只有一个线程能访问被修饰的部分。当一个线程访问被synchronized修饰的代码块或方法时,会先获取对象的锁,其他线程必须等待该锁被释放才能访问。例如在一个共享资源的读写操作中,用synchronized修饰读写方法,防止多线程同时读写导致数据不一致。
    • Lock接口:提供了更灵活的锁控制。比如可以实现可中断锁,在等待锁的过程中可以响应中断;还可以实现公平锁,按照线程请求锁的顺序来分配锁,避免某些线程一直得不到锁。例如ReentrantLock类实现了Lock接口,通过调用它的lock方法获取锁,unlock方法释放锁。
  • CountDownLatch和CyclicBarrier的区别
    • CountDownLatch:用于等待一组操作完成,计数器递减。例如有一个任务需要多个子任务完成后才能继续执行,每个子任务完成时调用CountDownLatch的countDown方法,主线程调用await方法等待,当计数器减为0时,主线程继续执行。
    • CyclicBarrier:让一组线程相互等待,直到所有线程都到达某个屏障点,计数器可循环使用。比如多个线程需要一起完成某个任务,每个线程执行到一定阶段后调用CyclicBarrier的await方法等待,当所有线程都到达await方法时,一起继续执行后续任务。
  • JVM内存结构
    • :存放对象实例,是垃圾回收的主要区域。分为新生代、老年代和永久代(Java 8后为元空间)。新生代又分为Eden区和两个Survivor区,新创建的对象一般存放在Eden区,经过几次垃圾回收后还存活的对象会被晋升到老年代。
    • :线程私有的,存放局部变量和方法调用的上下文。每个方法被调用时会创建一个栈帧,栈帧中包含局部变量表、操作数栈、动态链接、方法出口等信息。
    • 方法区:存储类信息、常量、静态变量等。在Java 8后,永久代被元空间取代,元空间使用本地内存,不受JVM堆大小限制。
    • 程序计数器:记录当前线程执行的字节码指令地址,是线程私有的,每个线程都有自己独立的程序计数器。
    • 本地方法栈:用于执行本地方法,即通过JNI(Java Native Interface)调用的非Java代码。
  • 垃圾回收主要针对的区域:主要针对堆中的对象进行回收。当对象不再被任何引用指向时,就会被垃圾回收器标记并回收。例如一个对象创建后,没有任何变量引用它,那么在垃圾回收时就会被当作垃圾回收掉。
  • 判断一个对象是否可以被回收的方法
    • 引用计数法:给对象添加一个引用计数器,当有引用指向该对象时计数器加1,引用消失时计数器减1,当计数器为0时对象可被回收。但这种方法无法解决循环引用的问题,比如两个对象相互引用,即使其他地方没有引用它们,计数器也不会为0,导致无法被回收。
    • 可达性分析算法:通过一系列的GC Roots对象作为起始点,从这些节点开始向下搜索,如果一个对象到GC Roots没有任何引用链相连,那么这个对象就是不可达的,可以被回收。GC Roots包括虚拟机栈中引用的对象、方法区中静态属性引用的对象、本地方法栈中引用的对象等。
  • 线程池的工作原理:预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。如果线程池中的线程都在忙碌,任务就会被放入队列中等待。线程池有几个重要参数:
    • 核心线程数:线程池正常工作时保持的线程数量。当提交的任务数小于核心线程数时,会创建新线程来执行任务。
    • 最大线程数:线程池允许的最大线程数量。当任务数超过核心线程数且队列已满时,会创建新线程直到线程数达到最大线程数。
    • 队列容量:用于存放任务的队列大小。当线程池中的线程都在执行任务时,新提交的任务会被放入队列中。
  • 合理设置线程池参数的方法
    • 任务执行时间:如果任务执行时间短,核心线程数可以设置小一些,因为不需要太多线程长时间占用资源。比如一些简单的计算任务。如果任务执行时间长,核心线程数可以设置大一些,避免任务长时间等待。例如复杂的数据库查询任务。
    • 系统资源情况:最大线程数要考虑系统的资源情况,不能设置太大导致资源耗尽。比如服务器的CPU核心数有限,如果设置的最大线程数过多,会导致CPU过度调度,性能下降。
    • 任务量:队列容量也要根据任务量来设置。如果任务量很大,队列容量可以设置大一些,防止任务堆积。如果任务量小,队列容量可以适当小一些,节省内存。
  • HashMap在多线程环境下的问题
    • 数据丢失:在多线程同时put元素时,可能会导致数据覆盖。比如两个线程同时计算出相同的哈希值,都要插入到同一个桶中,后插入的线程会覆盖先插入线程的数据。
    • 死循环:HashMap在扩容时可能会导致链表形成环形结构,从而导致死循环。当扩容时,需要重新计算元素的哈希值并插入到新的桶中,在这个过程中,如果链表的节点顺序处理不当,就可能形成环形链表,后续在遍历链表时就会陷入死循环。