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

40 阅读6分钟

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

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

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

王铁牛:线程池能提高线程的复用性,减少创建和销毁线程的开销。创建线程池可以用 ThreadPoolExecutor 类。

面试官:很好。再问你,HashMap 在多线程环境下会出现什么问题,如何解决?

王铁牛:呃……会出现数据丢失啥的吧,解决办法嘛……我想想,好像是用 ConcurrentHashMap 来替代。

面试官:第一轮面试结束,整体表现还行。接下来第二轮,说说 JVM 的内存结构。

王铁牛:JVM 内存结构包括堆、栈、方法区等。

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

王铁牛:类加载机制包括加载、验证、准备、解析、初始化。

面试官:JVM 如何进行垃圾回收的?

王铁牛:这个……好像是通过标记清除、标记整理、复制算法这些来回收垃圾。

面试官:第二轮面试结束。现在进入第三轮,讲讲 Spring 的核心特性。

王铁牛:Spring 有依赖注入、面向切面编程这些特性。

面试官:Spring Boot 相对于 Spring 有哪些优势?

王铁牛:Spring Boot 更简单,自动配置啥的,让开发更快。

面试官:说说 MyBatis 的工作原理。

王铁牛:就是通过 XML 配置文件或者注解来映射数据库操作。

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

答案:

  1. Java 多线程机制及创建线程
    • Java 多线程是指程序中包含多个执行单元,这些执行单元可以同时执行不同的任务。
    • 创建线程的方式主要有两种:
      • 继承 Thread 类:定义一个类继承 Thread 类,重写 run 方法,在 run 方法中编写线程执行的代码。然后创建该类的实例对象,调用 start 方法启动线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行的代码");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
 - 实现 Runnable 接口:定义一个类实现 Runnable 接口,实现 run 方法。然后创建 Thread 类的实例对象,并将实现了 Runnable 接口的类的实例作为参数传递给 Thread 类的构造函数,最后调用 start 方法启动线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行的代码");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}
  1. 线程池的作用及创建
    • 线程池的作用:
      • 提高线程的复用性,减少线程创建和销毁的开销,从而提高系统性能。
      • 便于线程管理,例如可以控制线程池中的线程数量,避免过多线程导致系统资源耗尽。
    • 创建线程池可以使用 ThreadPoolExecutor 类,示例代码如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小为 5 的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executorService.submit(() -> {
                System.out.println("执行任务 " + taskId);
            });
        }
        executorService.shutdown();
    }
}

这里通过 Executors.newFixedThreadPool(5) 创建了一个固定大小为 5 的线程池,后续提交的任务会在这 5 个线程中复用执行。 3. HashMap 在多线程环境下的问题及解决

  • HashMap 在多线程环境下可能会出现数据丢失、死循环等问题。
  • 原因是在扩容时,可能会导致链表形成环形结构,从而在 get 操作时出现死循环。
  • 解决办法是使用 ConcurrentHashMap,它采用了分段锁机制,在保证线程安全的同时提高了并发性能。例如:
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key1", 1);
        Integer value = map.get("key1");
        System.out.println(value);
    }
}
  1. JVM 的内存结构
    • JVM 内存结构主要包括:
      • 堆(Heap):是 JVM 中最大的一块内存区域,用于存储对象实例。它被划分为新生代、老年代等不同区域,不同区域有不同的垃圾回收策略。
      • 栈(Stack):每个线程都有自己独立的栈,用于存储局部变量、方法调用等信息。
      • 方法区(Method Area):用于存储类信息、常量、静态变量等。
      • 程序计数器(Program Counter Register):记录当前线程执行的字节码指令地址。
  2. 类加载机制的过程
    • 类加载机制包括以下几个过程:
      • 加载:将类的字节码文件从磁盘加载到内存中。
      • 验证:检查加载的字节码文件是否符合 JVM 的规范,确保其安全性。
      • 准备:为类的静态变量分配内存,并设置初始值。
      • 解析:将字节码文件中的符号引用转换为直接引用。
      • 初始化:执行类的静态代码块,为静态变量赋值等操作。
  3. JVM 垃圾回收
    • JVM 进行垃圾回收主要通过以下几种算法:
      • 标记清除算法:先标记出所有需要回收的对象,然后统一回收。这种算法会产生内存碎片。
      • 标记整理算法:先标记出需要回收的对象,然后将存活对象向一端移动,最后清理边界以外的内存。
      • 复制算法:将内存分为两块,每次只使用其中一块,当这一块内存满了,就将存活对象复制到另一块内存,然后清理原来的内存。
    • 例如,在新生代中,一般采用复制算法,因为新生代对象存活率低。老年代则可能采用标记清除或标记整理算法。
  4. Spring 的核心特性
    • 依赖注入(Dependency Injection):通过容器将依赖对象注入到目标对象中,降低对象之间的耦合度。例如:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
class UserService {
    private UserDao userDao;

    @Autowired
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}

@Component
class UserDao {
}

这里 UserService 通过 @Autowired 注解将 UserDao 注入进来。

  • 面向切面编程(Aspect Oriented Programming,AOP):通过预编译方式和运行期动态代理实现程序功能的统一维护。可以将一些横切关注点(如日志、事务管理等)从业务逻辑中分离出来。例如:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
class LogAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore() {
        System.out.println("方法执行前记录日志");
    }
}

这个切面类会在指定的业务方法执行前记录日志。 8. Spring Boot 相对于 Spring 的优势

  • Spring Boot 更简单,它采用了自动配置的机制,极大地减少了开发过程中的配置工作量。
  • 它内置了 Tomcat 等服务器,可直接运行,无需像 Spring 那样进行繁琐的服务器配置。
  • 提供了大量的 Starter 依赖,方便快速集成各种功能,例如 Spring Boot Starter Web 可以快速搭建一个 Web 应用。
  1. MyBatis 的工作原理
    • MyBatis 通过 XML 配置文件或者注解来映射数据库操作。
    • 其工作流程大致如下:
      • 读取配置文件,解析 SQL 语句和映射关系。
      • 根据映射关系创建 SqlSessionFactory。
      • 通过 SqlSessionFactory 创建 SqlSession。
      • 使用 SqlSession 执行 SQL 操作,如查询、插入、更新、删除等。
      • 例如,通过 XML 配置文件定义 SQL 语句:
<mapper namespace="com.example.mapper.UserMapper">
    <select id="getUserById" parameterType="int" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>

在 Java 代码中使用:

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

public class UserService {
    private SqlSessionFactory sqlSessionFactory;

    public UserService(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    public User getUserById(int id) {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            return sqlSession.selectOne("com.example.mapper.UserMapper.getUserById", id);
        }
    }
}

这里通过 MyBatis 的映射配置和 SqlSession 来执行查询数据库获取用户的操作。