《互联网大厂Java求职者面试大揭秘:核心知识与实战问答》

107 阅读9分钟

面试官:欢迎你来面试,先简单介绍下你自己吧。

王铁牛:面试官您好,我叫王铁牛,有两年Java开发经验,熟悉一些常见的Java技术。

面试官:好,第一轮提问开始。首先,Java 中如何创建一个线程?

王铁牛:可以通过继承Thread类或者实现Runnable接口来创建线程。继承Thread类需要重写run方法,实现Runnable接口也需要实现run方法,然后通过Thread类的构造函数传入Runnable对象来创建线程。

面试官:回答得不错。那说说线程池的核心参数有哪些?

王铁牛:线程池的核心参数有corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(线程存活时间)、unit(时间单位)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。

面试官:嗯,掌握得挺扎实。最后一个问题,多线程中如何保证线程安全?

王铁牛:可以使用synchronized关键字、Lock接口、原子类等方式来保证线程安全。

面试官:好,第一轮提问结束。接下来进入第二轮提问。说说HashMap的底层实现原理。

王铁牛:HashMap底层是基于数组和链表(JDK 1.8后引入红黑树)实现的。当插入一个元素时,先通过哈希函数计算出元素的哈希值,然后根据哈希值找到对应的数组下标。如果该下标为空,则直接插入新元素;如果不为空,则通过链表或红黑树来存储新元素。

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

王铁牛:ArrayList初始容量是10,当元素数量超过容量时,会进行扩容。扩容时新容量是原容量的1.5倍,如果新容量还不够,则直接新容量设为所需的大小。

面试官:不太准确。ArrayList扩容是新容量为原容量的1.5倍加1。再问个问题,Spring的核心特性有哪些?

王铁牛:Spring的核心特性有依赖注入、面向切面编程、IoC容器等。

面试官:第二轮提问完毕。现在是第三轮提问。说说MyBatis的工作原理。

王铁牛:MyBatis通过读取配置文件,解析SQL语句,然后通过反射机制创建对象,执行SQL操作,最后返回结果。

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

王铁牛:有Failover Cluster(失败自动切换)、Failfast Cluster(快速失败)、Failsafe Cluster(失败安全)、Failback Cluster(失败自动恢复)、Forking Cluster(并行调用多个服务)等。

面试官:回答得比较混乱。最后一个问题,Redis的数据结构有哪些?

王铁牛:有字符串、哈希、列表、集合、有序集合等。

面试官:好,面试结束。回去等通知吧。

答案:

  1. Java创建线程的方式
    • 继承Thread类:创建一个类继承Thread类,重写run方法,在run方法中编写线程执行的代码。然后通过创建该类的实例并调用start方法来启动线程。例如:
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("线程执行");
        }
    }
    MyThread thread = new MyThread();
    thread.start();
    
    • 实现Runnable接口:创建一个类实现Runnable接口,实现其中的run方法。然后通过Thread类的构造函数将该类的实例传入,再调用start方法启动线程。例如:
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("线程执行");
        }
    }
    MyRunnable runnable = new MyRunnable();
    Thread thread = new Thread(runnable);
    thread.start();
    
  2. 线程池的核心参数
    • corePoolSize(核心线程数):线程池创建时初始化的线程数,当提交的任务数小于corePoolSize时,线程池会创建新线程来执行任务。
    • maximumPoolSize(最大线程数):线程池能够容纳的最大线程数,当任务数大于corePoolSize且任务队列已满时,会创建新线程直到线程数达到maximumPoolSize。
    • keepAliveTime(线程存活时间):当线程数大于corePoolSize时,多余的线程在空闲时会存活的时间。
    • unit(时间单位):keepAliveTime的时间单位。
    • workQueue(任务队列):用于存放提交的任务,当线程池繁忙时,任务会被放入任务队列中。
    • threadFactory(线程工厂):用于创建线程,可自定义线程的名称、优先级等属性。
    • handler(拒绝策略):当线程池的线程数达到maximumPoolSize且任务队列已满时,新提交的任务会被拒绝,通过handler来处理拒绝的任务。常见的拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(调用者运行任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
  3. 多线程保证线程安全的方式
    • synchronized关键字:可以修饰方法或代码块。修饰方法时,同一时刻只能有一个线程访问该方法;修饰代码块时,同一时刻只能有一个线程进入该代码块。例如:
    public class SynchronizedExample {
        private int count = 0;
        public synchronized void increment() {
            count++;
        }
    }
    
    • Lock接口:通过Lock接口的实现类如ReentrantLock来实现线程同步。可以更灵活地控制锁的获取和释放,还可以实现公平锁等特性。例如:
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    public class LockExample {
        private int count = 0;
        private Lock lock = new ReentrantLock();
        public void increment() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
    }
    
    • 原子类:如AtomicInteger、AtomicLong等,它们提供了原子性的操作方法,不需要使用锁就可以保证操作的原子性。例如:
    import java.util.concurrent.atomic.AtomicInteger;
    public class AtomicExample {
        private AtomicInteger count = new AtomicInteger(0);
        public void increment() {
            count.incrementAndGet();
        }
    }
    
  4. HashMap的底层实现原理
    • HashMap底层基于数组和链表(JDK 1.8后引入红黑树)实现。
    • 哈希计算:插入元素时,首先通过哈希函数计算元素的哈希值。
    • 数组存储:根据哈希值通过取模运算得到数组下标,如果该下标为空,则直接将元素插入到该位置。
    • 链表或红黑树存储:如果下标不为空,说明有元素冲突,此时会将新元素插入到链表的尾部(JDK 1.8前)或红黑树中(JDK 1.8后,当链表长度大于8且数组长度大于64时,链表会转换为红黑树)。
    • 扩容机制:当HashMap中的元素个数超过容量*负载因子(默认0.75)时,会进行扩容。扩容时会创建一个新的更大的数组,将原数组中的元素重新计算哈希值后插入到新数组中。
  5. ArrayList的扩容机制
    • ArrayList初始容量是10。
    • 当元素数量超过容量时,会进行扩容。扩容时新容量是原容量的1.5倍加1。
    • 例如,原容量为10,当插入第11个元素时,新容量变为10 * 1.5 + 1 = 16。然后将原数组中的元素复制到新数组中。
  6. Spring的核心特性
    • 依赖注入(Dependency Injection):通过IoC容器将对象之间的依赖关系注入到对象中,降低了对象之间的耦合度。例如,一个Service类依赖于一个Dao类,通过Spring可以在配置文件中配置将Dao类的实例注入到Service类中。
    • 面向切面编程(Aspect - Oriented Programming,AOP):可以将一些横切关注点(如日志记录、事务管理等)与业务逻辑分离,通过切面来实现这些功能。比如在方法执行前后记录日志,在业务方法上添加事务注解来实现事务管理。
    • IoC容器:负责创建、配置和管理对象,管理对象之间的依赖关系,使得对象的创建和使用更加方便和灵活。
  7. MyBatis的工作原理
    • 读取配置文件:MyBatis首先读取配置文件,配置文件中包含了数据库连接信息、SQL映射等内容。
    • 解析SQL语句:通过解析器将SQL映射文件中的SQL语句进行解析,将其封装成MappedStatement对象。
    • 反射机制创建对象:利用反射机制根据配置信息创建相应的对象,如SqlSessionFactory、SqlSession等。
    • 执行SQL操作:通过SqlSession对象来执行SQL语句,SqlSession会根据MappedStatement对象找到对应的SQL语句并执行,执行过程中会处理参数传递、结果集映射等。
    • 返回结果:将SQL执行结果按照配置的结果集映射规则进行映射,返回给调用者。
  8. Dubbo的集群容错模式
    • Failover Cluster(失败自动切换):当调用失败时,会自动切换到其他服务提供者进行重试,默认重试次数是2次。适用于读操作等对一致性要求不高的场景。
    • Failfast Cluster(快速失败):如果调用失败,直接抛出异常,不会进行重试。适用于幂等性操作,如新增数据时如果失败不希望重试。
    • Failsafe Cluster(失败安全):调用失败时,直接忽略,不抛出异常,也不进行重试。适用于写审计日志等操作,即使失败也不影响业务流程。
    • Failback Cluster(失败自动恢复):调用失败时,记录失败请求,然后定时重试。适用于消息通知等操作,允许一定时间内的失败并自动恢复。
    • Forking Cluster(并行调用多个服务):同时调用多个服务提供者,只要有一个成功就返回,用于对实时性要求较高的场景,比如实时查询多个数据源获取数据。
  9. Redis的数据结构
    • 字符串(String):最基本的数据结构,能存储各种类型的数据,如整数、字符串等。可以用于缓存、计数器、分布式锁等场景。例如:
    SET key value
    GET key
    INCR key  // 自增操作
    
    • 哈希(Hash):适合存储对象,是一个键值对集合。可以将对象的属性和值存储在哈希中。例如:
    HMSET user:1 name "张三" age 20
    HGETALL user:1
    
    • 列表(List):按照插入顺序排序的列表,可以从列表两端进行操作。常用于消息队列、任务队列等场景。例如:
    LPUSH mylist element1 element2
    RPOP mylist
    
    • 集合(Set):无序的唯一元素集合。可以用于去重、交集、并集等操作。例如:
    SADD myset element1 element2
    SMEMBERS myset
    SINTER set1 set2  // 求交集
    
    • 有序集合(Sorted Set):集合中的元素是有序的,通过分数进行排序。可以用于排行榜等场景。例如:
    ZADD myzset 10 element1 20 element2
    ZRANGE myzset 0 -1 WITHSCORES