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

44 阅读7分钟

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

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

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

王铁牛:可以用 synchronized 关键字啊,对共享资源加锁,这样同一时间只有一个线程能访问。

面试官:很好。接下来问几个关于 JUC 的问题,什么是 AQS?

王铁牛:这个……嗯……好像是和同步队列有关吧。

面试官:第一轮面试结束,回去等通知吧。

面试官:JVM 内存模型主要分为哪几个部分?

王铁牛:有程序计数器、虚拟机栈、本地方法栈、堆和方法区。

面试官:那类加载机制的过程是怎样的?

王铁牛:先加载,然后验证,接着准备,再解析,最后初始化。

面试官:如何排查 JVM 内存泄漏问题?

王铁牛:呃……可以用工具查看内存占用情况吧。

面试官:第二轮面试结束,回家等通知。

面试官:多线程中,线程池有什么作用?

王铁牛:能复用线程,提高效率,减少创建和销毁线程的开销。

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

王铁牛:这得根据任务的类型和数量来定吧,我也不太清楚具体怎么配。

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

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

面试官:ArrayList 是线程安全的吗?

王铁牛:不是,它不是线程安全的。

面试官:第三轮面试结束,回家等通知。

答案:

  1. 面向对象三大特性
    • 封装:将数据和操作数据的方法捆绑在一起,对外提供统一的访问接口,隐藏内部实现细节。这样可以提高代码的安全性和可维护性,比如一个类中的私有成员变量,通过公有的 getter 和 setter 方法来访问和修改。
    • 继承:子类继承父类的属性和方法,实现代码复用。子类可以扩展和重写父类的方法,比如一个 Animal 类有 eat 方法,Dog 类继承 Animal 类后可以重写 eat 方法,体现狗独特的进食方式。
    • 多态:同一个行为具有多个不同表现形式。通过方法重写和接口实现来体现,比如一个 Animal 类型的变量可以指向 Dog 对象或者 Cat 对象,调用它们各自的 eat 方法时会有不同的表现。
  2. 多线程环境下保证数据一致性
    • synchronized 关键字:它可以修饰代码块或者方法。当一个线程访问被 synchronized 修饰的代码块或方法时,会先获取对象的锁。如果锁被其他线程持有,该线程就会进入等待状态,直到锁被释放。这样就能保证同一时间只有一个线程能访问共享资源,从而保证数据一致性。例如在一个银行账户类中,对存款和取款方法用 synchronized 修饰,防止多个线程同时操作导致数据错误。
  3. AQS(AbstractQueuedSynchronizer)
    • 它是一个用于构建锁和同步器的框架。它使用一个 FIFO 队列来管理等待获取锁的线程。核心是通过一个 state 变量来表示同步状态,比如 0 表示未锁定,1 表示锁定。线程通过调用 acquire 方法尝试获取锁,如果获取不到就会被加入到等待队列中。当锁被释放时,会从队列中唤醒一个等待线程。很多 JUC 包中的类如 ReentrantLock、Semaphore 等都是基于 AQS 实现的。
  4. JVM 内存模型主要部分
    • 程序计数器:记录当前线程执行的字节码指令地址,是线程私有的。
    • 虚拟机栈:每个方法执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,也是线程私有的。
    • 本地方法栈:与虚拟机栈类似,用于执行本地方法,是线程私有的。
    • :是 JVM 中最大的一块内存区域,被所有线程共享,用于存储对象实例和数组。
    • 方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  5. 类加载机制过程
    • 加载:通过类的全限定名获取定义此类的二进制字节流,将其加载到内存中,并创建一个 java.lang.Class 对象。
    • 验证:确保加载的字节流符合 JVM 规范,不会危害虚拟机安全。
    • 准备:为类的静态变量分配内存,并设置默认初始值。
    • 解析:将常量池中的符号引用替换为直接引用。
    • 初始化:执行类构造器 方法,对静态变量进行初始化赋值等操作。
  6. 排查 JVM 内存泄漏问题
    • 使用工具查看内存占用情况:比如 jconsole、VisualVM 等工具。通过这些工具可以监控堆内存的使用情况,查看哪些对象占用了大量内存且长时间不被释放。如果发现某个对象的实例数量不断增加且没有被正常回收,可能就存在内存泄漏。还可以通过分析对象的引用关系,找出是否存在长生命周期的对象持有短生命周期对象的引用,导致短生命周期对象无法被回收。
  7. 线程池作用
    • 复用线程:线程池创建后会预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务,而不是每次都创建新线程。这样可以大大减少创建和销毁线程的开销,提高系统性能。
    • 提高效率:避免了频繁创建和销毁线程带来的资源消耗和时间开销,使得任务可以更快地得到执行。
    • 便于管理:可以统一管理线程的生命周期、任务队列等,方便进行监控和调优。
  8. 合理配置线程池参数
    • corePoolSize:核心线程数,当提交的任务数小于 corePoolSize 时,线程池会创建新线程来执行任务。
    • maximumPoolSize:最大线程数,当提交的任务数大于 corePoolSize 且任务队列已满时,会创建新线程直到线程数达到 maximumPoolSize。
    • keepAliveTime:线程池中的线程在空闲时的存活时间,当线程空闲时间超过 keepAliveTime 时,非核心线程会被销毁。
    • unit:keepAliveTime 的时间单位。
    • workQueue:任务队列,用于存放提交的任务,当线程池中的线程都在忙碌时,新提交的任务会被放入任务队列中。
    • threadFactory:线程工厂,用于创建线程,可自定义线程的名称、优先级等属性。
    • handler:拒绝策略,当线程池的线程数达到 maximumPoolSize 且任务队列已满时,会调用拒绝策略来处理新提交的任务。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(调用者运行任务)、DiscardPolicy(丢弃新提交的任务)、DiscardOldestPolicy(丢弃队列中最旧的任务)。
  9. HashMap 在多线程环境下问题
    • 在多线程环境下,当多个线程同时对 HashMap 进行 put 操作时,可能会导致链表成环,进而在获取元素时造成死循环。这是因为 HashMap 在扩容时会重新计算节点的位置,如果多个线程同时进行扩容操作,就可能出现链表形成环形结构。
  10. ArrayList 线程安全性
    • ArrayList 不是线程安全的。它的 add、remove 等方法没有进行同步处理。在多线程环境下同时对 ArrayList 进行读写操作时,可能会出现数据不一致的情况,比如一个线程在遍历 ArrayList 时,另一个线程同时删除了某个元素,就会导致遍历异常。