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

33 阅读8分钟

面试官:请简要介绍一下Java中的多线程,以及如何创建一个线程。

王铁牛:多线程就是在一个程序中同时运行多个线程。创建线程可以通过继承Thread类或者实现Runnable接口。

面试官:不错,回答得很简洁明了。那再问你,线程池有什么作用?如何创建一个线程池?

王铁牛:线程池可以复用线程,提高性能。创建线程池可以使用ThreadPoolExecutor类。

面试官:很好。接下来问一些关于JVM的问题,JVM的内存模型分为哪几个部分?

王铁牛:JVM内存模型分为堆、栈、方法区、程序计数器等。

第一轮结束,王铁牛对简单问题回答得较为清晰,得到了面试官的夸赞。

面试官:那说说HashMap的底层实现原理。

王铁牛:嗯……它是基于数组和链表实现的,当链表长度超过一定阈值时会转换为红黑树。

面试官:那扩容机制是怎样的呢?

王铁牛:扩容就是数组大小翻倍,然后重新计算元素位置。

面试官:Spring框架中,依赖注入有几种方式?

王铁牛:有构造器注入、setter方法注入等。

第二轮结束,王铁牛回答得有好有坏。

面试官:Dubbo的集群容错有哪些模式?

王铁牛:这个……好像有几种,但是我不太记得了。

面试官:RabbitMq的消息确认机制了解吗?

王铁牛:不太清楚。

面试官:xxl-job的执行器有哪些类型?

王铁牛:呃……不太明白。

第三轮结束,王铁牛对于复杂问题回答得比较混乱。

面试结束,面试官表示会让王铁牛回家等通知。此次面试中,王铁牛在面对简单问题时能够清晰作答,展现出了一定的基础知识储备,但在面对复杂问题时,回答得不够准确和清晰,需要进一步加强对相关技术的深入理解和掌握。

答案:

  1. 多线程及创建线程
    • 多线程:多线程是指在一个程序中同时运行多个线程,每个线程执行不同的任务,从而提高程序的执行效率和响应速度。
    • 创建线程的方式
      • 继承Thread类:定义一个类继承Thread类,重写run方法,然后创建该类的对象并调用start方法启动线程。例如:
        class MyThread extends Thread {
            @Override
            public void run() {
                System.out.println("This is a thread");
            }
        }
        MyThread thread = new MyThread();
        thread.start();
        
      • 实现Runnable接口:定义一个类实现Runnable接口,实现run方法,然后创建Thread类的对象并将实现Runnable接口的类的对象作为参数传递给Thread类的构造函数,最后调用start方法启动线程。例如:
        class MyRunnable implements Runnable {
            @Override
            public void run() {
                System.out.println("This is a runnable thread");
            }
        }
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        
  2. 线程池
    • 作用:线程池可以复用线程,避免频繁创建和销毁线程带来的性能开销。它可以控制线程的并发数量,提高系统的稳定性和响应速度。
    • 创建线程池:使用ThreadPoolExecutor类来创建线程池。例如:
      ThreadPoolExecutor executor = new ThreadPoolExecutor(
              corePoolSize,
              maximumPoolSize,
              keepAliveTime,
              TimeUnit.SECONDS,
              new LinkedBlockingQueue<>(),
              new ThreadPoolExecutor.CallerRunsPolicy());
      
      • 参数解释
        • corePoolSize:线程池的核心线程数,当提交的任务数小于corePoolSize时,会创建新的线程来执行任务。
        • maximumPoolSize:线程池的最大线程数,当提交的任务数大于corePoolSize且任务队列已满时,会创建新的线程来执行任务,直到线程数达到maximumPoolSize。
        • keepAliveTime:线程池中的线程在空闲时的存活时间,当线程空闲时间超过keepAliveTime时,线程会被销毁。
        • TimeUnit:keepAliveTime的时间单位。
        • workQueue:任务队列,用于存储提交的任务,当线程池中的线程都在忙碌时,新提交的任务会被放入任务队列中。
        • handler:线程池的拒绝策略,当线程池中的线程数达到maximumPoolSize且任务队列已满时,会调用handler来处理新提交的任务。
  3. JVM内存模型
    • :是JVM中最大的一块内存区域,用于存储对象实例。它被划分为新生代、老年代和永久代(Java 8及以后为元空间)。新生代又分为Eden区和两个Survivor区,对象一般首先在Eden区创建,当Eden区满时,会触发Minor GC,将存活的对象移动到Survivor区,当Survivor区也满时,会将对象移动到老年代。老年代用于存储生命周期较长的对象。
    • :每个线程都有自己独立的栈,用于存储局部变量、方法调用等信息。栈内存是线程私有的,随着线程的创建而创建,随着线程的结束而销毁。
    • 方法区:用于存储类的信息、常量、静态变量等。在Java 8及以后,方法区被元空间取代,元空间使用本地内存,而不是像方法区那样使用JVM的堆内存。
    • 程序计数器:是一块较小的内存区域,它记录了当前线程正在执行的字节码指令的地址,每个线程都有自己独立的程序计数器。
  4. HashMap底层实现原理
    • HashMap是基于数组和链表(JDK 8及以后引入红黑树)实现的。
    • 初始数组:HashMap内部维护一个Entry数组,初始容量为16(默认值)。
    • 哈希值计算:通过key的hashCode方法计算出哈希值,然后通过扰动函数对哈希值进行进一步处理,使其分布更均匀。
    • 存储位置计算:通过哈希值与数组长度进行按位与操作,得到元素在数组中的存储位置。例如:index = hash & (table.length - 1)
    • 冲突处理
      • 链表:当两个元素的哈希值通过上述计算得到相同的存储位置时,会在该位置形成链表。新元素会添加到链表的头部。
      • 红黑树:当链表长度超过一定阈值(默认8)时,链表会转换为红黑树,以提高查询效率。
    • 扩容机制
      • 当HashMap中的元素个数超过负载因子(默认0.75)与数组长度的乘积时,会触发扩容。
      • 扩容时,会创建一个新的更大的数组(大小翻倍),然后将原数组中的元素重新计算哈希值并放入新数组中。
  5. Spring框架依赖注入方式
    • 构造器注入:通过构造函数将依赖对象注入到目标对象中。例如:
      public class UserService {
          private final UserDao userDao;
          public UserService(UserDao userDao) {
              this.userDao = userDao;
          }
      }
      
    • setter方法注入:通过setter方法将依赖对象注入到目标对象中。例如:
      public class UserService {
          private UserDao userDao;
          public void setUserDao(UserDao userDao) {
              this.userDao = userDao;
          }
      }
      
  6. Dubbo集群容错模式
    • Failover Cluster:失败自动切换,当出现调用失败时,会自动重试其他服务器。
    • Failfast Cluster:快速失败,当调用失败时,立即返回错误,不再重试。
    • Failsafe Cluster:失败安全,当调用失败时,直接忽略,不抛出异常。
    • Failback Cluster:失败自动恢复,当调用失败时,会记录失败请求,然后定时重试。
    • Forking Cluster:并行调用多个服务器,只要有一个成功就返回。
  7. RabbitMq消息确认机制
    • 生产者确认机制
      • 事务机制:通过channel.txSelect开启事务,然后发送消息,最后通过channel.txCommit提交事务,如果发送过程中出现异常,可以通过channel.txRollback回滚事务。这种方式比较简单,但性能较低。
      • Confirm机制:通过channel.confirmSelect开启确认模式,发送消息后,通过addConfirmListener监听消息的确认结果。Confirm机制有两种模式:
        • 普通确认模式:消息发送后,服务器会异步返回确认结果。
        • 批量确认模式:可以将多条消息一起发送,然后一次性获取这些消息的确认结果。
    • 消费者确认机制
      • 自动确认:消费者接收到消息后,自动确认消息已被接收。通过channel.basicConsume(queue, true, consumerTag, consumer)方法的第二个参数设置为true来开启自动确认。
      • 手动确认:消费者接收到消息后,需要手动调用channel.basicAck(deliveryTag, false)方法来确认消息已被接收。其中deliveryTag是消息的唯一标识,false表示不批量确认。如果消息处理失败,可以调用channel.basicNack(deliveryTag, false, true)方法拒绝消息,true表示重新入队。
  8. xxl - job执行器类型
    • BEAN模式:任务以Spring Bean的方式运行,适用于轻量级任务。
    • GLUE模式:用户可以在线编写任务逻辑,支持多种语言,任务运行时会动态编译执行。
    • CRON模式:按照指定的Cron表达式定时执行任务。
    • HTTP模式:通过HTTP请求触发任务执行。
    • SDK模式:适用于集成到其他应用中,通过调用SDK提供的接口触发任务。