面试官:第一轮提问开始。首先,讲讲Java中的多线程有哪些实现方式?
王铁牛:多线程实现方式有继承Thread类、实现Runnable接口,还有实现Callable接口通过FutureTask获取结果。
面试官:不错,回答正确。那线程池有哪几种类型?
王铁牛:有FixedThreadPool固定大小线程池、CachedThreadPool可缓存线程池、ScheduledThreadPool定时线程池、SingleThreadExecutor单线程线程池。
面试官:很好。再问一个,HashMap在多线程环境下有什么问题?
王铁牛:在多线程环境下可能会出现链表形成环形结构,导致死循环,还有数据覆盖问题。
面试官:第一轮提问结束。下面进行第二轮提问。说说Spring的核心特性有哪些?
王铁牛:Spring的核心特性有依赖注入、面向切面编程、IoC容器等。
面试官:那Spring Boot的自动配置原理是什么?
王铁牛:Spring Boot通过条件注解来实现自动配置,根据类路径下的依赖来自动配置相关组件。
面试官:不太准确。再问,MyBatis的缓存机制是怎样的?
王铁牛:MyBatis有一级缓存和二级缓存,一级缓存是SqlSession级别的,二级缓存是namespace级别的。
面试官:第二轮提问完毕。现在是第三轮提问。Dubbo的集群容错策略有哪些?
王铁牛:有Failover Cluster失败自动切换、Failfast Cluster快速失败、Failsafe Cluster失败安全、Failback Cluster失败自动恢复等。
面试官:RabbitMq的消息确认机制了解吗?
王铁牛:不太清楚。
面试官:xxl-job的执行流程能说一下吗?
王铁牛:乱说一通,没有说清楚。
面试官:第三轮提问结束。整体来看,你对一些简单问题回答得还可以,但对于复杂问题的理解和回答不够准确和清晰。回家等通知吧。
答案:
- Java多线程实现方式:
- 继承Thread类:创建一个类继承Thread类,重写run方法,在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();- 实现Callable接口:创建一个类实现Callable接口,重写call方法,该方法可以有返回值。然后创建FutureTask类的实例,并将实现Callable接口的类的实例作为参数传递给FutureTask类的构造函数。再创建Thread类的实例,并将FutureTask类的实例作为参数传递给Thread类的构造函数,最后调用start方法启动线程。通过FutureTask的get方法获取线程执行的返回值。例如:
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "This is a callable thread result."; } } MyCallable callable = new MyCallable(); FutureTask<String> futureTask = new FutureTask<>(callable); Thread thread = new Thread(futureTask); thread.start(); try { String result = futureTask.get(); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } - 线程池类型:
- FixedThreadPool:固定大小的线程池,核心线程数和最大线程数相同。当提交的任务数小于核心线程数时,由核心线程执行任务;当任务数大于核心线程数时,将任务放入阻塞队列;如果阻塞队列已满且线程数小于最大线程数,则创建新线程执行任务;如果线程数达到最大线程数,任务将根据拒绝策略处理。例如:
ExecutorService executorService = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { executorService.submit(() -> { System.out.println(Thread.currentThread().getName() + " is running."); }); } executorService.shutdown();- CachedThreadPool:可缓存线程池,核心线程数为0,最大线程数为Integer.MAX_VALUE。线程池会缓存线程,当有任务提交时,如果有空闲线程则复用,没有则创建新线程。线程空闲60秒后会被回收。例如:
ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { executorService.submit(() -> { System.out.println(Thread.currentThread().getName() + " is running."); }); } executorService.shutdown();- ScheduledThreadPool:定时线程池,核心线程数固定,支持定时执行任务。可以通过ScheduledExecutorService接口的schedule方法定时执行任务,或者通过scheduleAtFixedRate方法按固定速率执行任务,通过scheduleWithFixedDelay方法按固定延迟执行任务。例如:
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5); executorService.schedule(() -> { System.out.println("This is a scheduled task."); }, 2, TimeUnit.SECONDS); executorService.shutdown();- SingleThreadExecutor:单线程线程池,核心线程数和最大线程数都为1。保证所有任务按照顺序执行,一个时间点只能执行一个任务。例如:
ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { executorService.submit(() -> { System.out.println(Thread.currentThread().getName() + " is running."); }); } executorService.shutdown(); - HashMap在多线程环境下的问题:
- 链表形成环形结构导致死循环:在JDK1.7及以前的版本中,HashMap在扩容时采用头插法。当多个线程同时对HashMap进行扩容操作时,可能会导致链表形成环形结构。例如,线程A和线程B同时对HashMap进行扩容,在重新计算节点位置和插入新节点的过程中,可能会出现链表的前后顺序被打乱,最终形成环形结构。当后续有线程访问该链表时,就会陷入死循环。
- 数据覆盖问题:当多个线程同时对HashMap进行put操作时,如果计算出的哈希值相同,就会在链表或红黑树中添加新节点。如果此时另一个线程也对相同位置进行put操作,就可能会覆盖之前的数据。
- Spring的核心特性:
- 依赖注入(Dependency Injection):通过控制反转(IoC)容器,将对象的依赖关系由程序主动创建改为由容器注入。例如,一个Service类依赖于一个Dao类,在传统方式下,Service类需要自己创建Dao类的实例。而在Spring中,可以通过配置文件或注解,让Spring容器创建Dao类的实例并注入到Service类中。
- 面向切面编程(Aspect - Oriented Programming,AOP):通过预编译方式和运行期动态代理实现程序功能的统一维护。可以将一些通用的功能(如日志记录、事务管理等)封装成切面,在不修改业务逻辑代码的情况下,将切面织入到目标对象的方法执行过程中。例如,使用@Aspect注解定义切面类,通过切入点表达式指定在哪些方法上应用切面逻辑。
- IoC容器:管理应用程序中的对象,负责对象的创建、配置和组装。IoC容器通过读取配置文件或注解,创建对象并将它们的依赖关系注入到需要的地方。比如基于XML的配置文件或基于注解的配置方式来定义Bean及其依赖关系。
- Spring Boot自动配置原理: Spring Boot通过条件注解来实现自动配置。它会扫描类路径下的所有依赖,根据这些依赖来自动配置相关组件。例如,当项目中引入了Spring Data JPA的依赖时,Spring Boot会自动配置JpaRepositoriesAutoConfiguration等配置类。这些配置类通过@Conditional注解来判断条件是否满足。如果条件满足,就会自动配置相应的组件,如创建EntityManagerFactory、注册JpaRepository等。@Conditional注解可以基于类是否存在、Bean是否存在、环境变量等条件进行判断。比如@ConditionalOnClass注解表示当某个类存在时才进行配置,@ConditionalOnBean注解表示当某个Bean存在时才进行配置。这样Spring Boot就能根据项目的依赖自动配置合适的组件,大大简化了开发过程。
- MyBatis的缓存机制:
- 一级缓存:是SqlSession级别的缓存。在同一个SqlSession中,当执行相同的查询语句时,会直接从缓存中获取数据,而不会再次查询数据库。例如,在一个SqlSession中,第一次执行“select * from user where id = 1”的查询,结果会被缓存到一级缓存中。当再次执行相同的查询时,就会从缓存中获取结果,而不会再次访问数据库。一级缓存的生命周期是SqlSession的生命周期,当SqlSession关闭时,一级缓存也会被清空。
- 二级缓存:是namespace级别的缓存。多个SqlSession可以共享二级缓存。当一个SqlSession执行查询操作并将结果放入二级缓存后,其他SqlSession如果执行相同的查询,就可以从二级缓存中获取数据。例如,在一个Mapper.xml文件中定义了一个查询方法,当一个SqlSession执行该查询并将结果放入二级缓存后,其他SqlSession在执行相同查询时,只要二级缓存中有数据,就会直接使用缓存数据。二级缓存的开启需要在MyBatis的配置文件中进行配置,并且Mapper.xml文件中需要使用标签来启用二级缓存。二级缓存的更新是通过事务的提交来触发的,当事务提交时,会将一级缓存中的数据同步到二级缓存中。
- Dubbo的集群容错策略:
- Failover Cluster(失败自动切换):当调用失败后,会自动切换到其他服务提供者进行重试。默认重试次数为2次。比如调用一个服务时,第一次调用失败,会自动重试调用其他可用的服务提供者,如果重试后还是失败,才会返回失败信息给调用者。
- Failfast Cluster(快速失败):调用失败后,直接快速返回失败,不会进行重试。适用于幂等操作,即多次调用对系统状态没有影响的操作。例如,查询操作如果失败,直接返回失败,不会重试。
- Failsafe Cluster(失败安全):调用失败后,直接忽略失败,继续执行后续操作,不返回错误信息给调用者。适用于写审计日志等操作,即使失败也不影响整体业务流程。比如记录用户操作日志,即使记录失败也不影响业务的正常运行。
- Failback Cluster(失败自动恢复):调用失败后,会自动记录失败请求,然后定时重试。适用于对可靠性要求较高的场景,如消息发送等。例如,发送消息失败后,会将失败的消息记录下来,然后在后台定时重试发送。
- RabbitMq的消息确认机制:
- 生产者确认机制:
- 事务机制:生产者发送消息之前开启事务(channel.txSelect()),然后发送消息。如果消息发送成功,提交事务(channel.txCommit());如果发送失败,回滚事务(channel.txRollback())。例如:
channel.txSelect(); channel.basicPublish("", "queueName", null, "message".getBytes()); channel.txCommit();- Confirm机制:生产者将信道设置成confirm模式(channel.confirmSelect()),发送消息后,通过监听ConfirmCallback来确认消息是否发送成功。如果发送成功,会调用confirm方法;如果发送失败,会调用handleNack方法。例如:
channel.confirmSelect(); channel.addConfirmListener(new ConfirmListener() { @Override public void handleAck(long deliveryTag, boolean multiple) throws IOException { System.out.println("消息发送成功,deliveryTag: " + deliveryTag); } @Override public void handleNack(long deliveryTag, boolean multiple) throws IOException { System.out.println("消息发送失败,deliveryTag: " + deliveryTag); } }); channel.basicPublish("", "queueName", null, "message".getBytes()); - 消费者确认机制:
- 自动确认:消费者在接收到消息后,自动确认消息已接收。可以通过在basicConsume方法中设置autoAck为true来实现。例如:
channel.basicConsume("queueName", true, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("Received message: " + new String(body)); } });- 手动确认:消费者接收到消息后,不自动确认,而是通过调用basicAck方法来手动确认消息已接收。例如:
channel.basicConsume("queueName", false, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { System.out.println("Received message: " + new String(body)); channel.basicAck(envelope.getDeliveryTag(), false); } catch (IOException e) { e.printStackTrace(); } } });
- 生产者确认机制:
- xxl - job的执行流程:
- 调度中心:负责管理任务调度规则、触发调度等。调度中心配置了任务的执行时间、执行频率等调度策略。
- 执行器:负责实际执行任务。执行器会定时从调度中心拉取任务。
- 流程:调度中心根据配置的调度策略,在指定时间触发任务调度。将任务信息发送给对应的执行器。执行器接收到任务后,根据任务类型(如Java类任务、Shell脚本任务等),调用相应的执行逻辑来执行任务。任务执行完成后,执行器会将执行结果反馈给调度中心。例如,配置了一个定时执行的Java类任务,调度中心在定时时间触发调度,将任务信息发送给执行器,执行器启动对应的Java程序来执行任务,任务执行结束后,将任务执行的状态(成功、失败等)返回给调度中心。