博客记录-day099-Java并发面试题+Spring面试题

110 阅读12分钟

一、小林-Java并发面试题

1、volatile可以保证线程安全吗?

volatile关键字可以保证可见性,但不能保证原子性,因此不能完全保证线程安全。volatile关键字用于修饰变量,当一个线程修改了volatile修饰的变量的值,其他线程能够立即看到最新的值,从而避免了线程之间的数据不一致。

但是,volatile并不能解决多线程并发下的复合操作问题,比如i++这种操作不是原子操作,如果多个线程同时对i进行自增操作,volatile不能保证线程安全。对于复合操作,需要使用synchronized关键字或者Lock来保证原子性和线程安全。

2、非公平锁吞吐量为什么比公平锁大?

  • 公平锁执行流程:获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
  • 非公平锁执行流程:当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。

3、介绍一下线程池的工作原理

线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗,线程池的工作原理如下图:

img

线程池分为核心线程池,线程池的最大容量,还有等待任务的队列,提交一个任务,如果核心线程没有满,就创建一个线程,如果满了,就是会加入等待队列,如果等待队列满了,就会增加线程,如果达到最大线程数量,如果都达到最大线程数量,就会按照一些丢弃的策略进行处理。

任务执行流程如下:

提交任务 → 核心线程是否已满?
  ├─ 未满 → 创建核心线程执行
  └─ 已满 → 任务入队
       ├─ 队列未满 → 等待执行
       └─ 队列已满 → 创建非核心线程
           ├─ 未达最大线程数 → 执行任务
           └─ 已达最大线程数 → 执行拒绝策略

4、线程池的参数有哪些?

线程池的构造函数有7个参数:

img

  • corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
  • maximumPoolSize:线程池中最多可容纳的线程数量。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程且当前线程池的线程数量小于corePoolSize,就会创建新的线程来执行任务,否则就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。
  • keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
  • unit:就是keepAliveTime时间的单位。
  • workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
  • threadFactory:线程工厂。可以用来给线程取名字等等
  • handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略

5、线程池工作队列满了有哪些拒接策略?

当线程池的任务队列满了之后,线程池会执行指定的拒绝策略来应对,常用的四种拒绝策略包括:CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy,此外,还可以通过实现RejectedExecutionHandler接口来自定义拒绝策略。

四种预置的拒绝策略:

  • CallerRunsPolicy,使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。
  • AbortPolicy,直接抛出一个任务被线程池拒绝的异常。
  • DiscardPolicy,不做任何处理,静默拒绝提交的任务。
  • DiscardOldestPolicy,抛弃最老的任务,然后执行该任务。
  • 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略。

6、有线程池参数设置的经验吗?

核心线程数(corePoolSize)设置的经验:

  • CPU密集型:corePoolSize = CPU核数 + 1(避免过多线程竞争CPU)
  • IO密集型:corePoolSize = CPU核数 x 2(或更高,具体看IO等待时间)

场景一:电商场景,特点瞬时高并发、任务处理时间短,线程池的配置可设置如下:

new ThreadPoolExecutor(
    16,                     // corePoolSize = 16(假设8核CPU × 2)
    32,                     // maximumPoolSize = 32(突发流量扩容)
    10, TimeUnit.SECONDS,   // 非核心线程空闲10秒回收
    new SynchronousQueue<>(), // 不缓存任务,直接扩容线程
    new AbortPolicy()       // 直接拒绝,避免系统过载
);

说明:

  • 使用SynchronousQueue确保任务直达线程,避免队列延迟。
  • 拒绝策略快速失败,前端返回“活动火爆”提示,结合降级策略(如缓存预热)。

场景二:后台数据处理服务,特点稳定流量、任务处理时间长(秒级)、允许一定延迟,线程池的配置可设置如下:

new ThreadPoolExecutor(
    8,                      // corePoolSize = 8(8核CPU)
    8,                      // maximumPoolSize = 8(禁止扩容,避免资源耗尽)
    0, TimeUnit.SECONDS,    // 不回收线程
    new ArrayBlockingQueue<>(1000), // 有界队列,容量1000
    new CallerRunsPolicy()  // 队列满后由调用线程执行
);

说明:

  • 固定线程数避免资源波动,队列缓冲任务,拒绝策略兜底。
  • 配合监控告警(如队列使用率>80%触发扩容)。

场景三:微服务HTTP请求处理,特点IO密集型、依赖下游服务响应时间,线程池的配置可设置如下:

new ThreadPoolExecutor(
    16,                     // corePoolSize = 16(8核 × 2)
    64,                     // maximumPoolSize = 64(应对慢下游)
    60, TimeUnit.SECONDS,   // 非核心线程空闲60秒回收
    new LinkedBlockingQueue<>(200), // 有界队列容量200
    new CustomRetryPolicy() // 自定义拒绝策略(重试或降级)
);

说明:

  • 根据下游RT(响应时间)调整线程数,队列防止瞬时峰值。
  • 自定义拒绝策略将任务暂存Redis,异步重试。

7、核心线程数设置为0可不可以?

可以,当核心线程数为0的时候,会创建一个非核心线程进行执行。

从下面的源码也可以看到,当核心线程数为 0 时,来了一个任务之后,会先将任务添加到任务队列,同时也会判断当前工作的线程数是否为 0,如果为 0,则会创建线程来执行线程池的任务。

image-20240820113849549

8、线程池种类有哪些?

  • ScheduledThreadPool:可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔 10 秒钟执行一次任务,我通过这个实现类设置定期执行任务的策略。
  • FixedThreadPool:它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。
  • CachedThreadPool:可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。
  • SingleThreadExecutor:它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。
  • SingleThreadScheduledExecutor:它实际和 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程。

9、线程池中shutdown (),shutdownNow()这两个方法有什么作用?

从下面的源码【高亮】注释可以很清晰的看出两者的区别:

  • shutdown使用了以后会置状态为SHUTDOWN,正在执行的任务会继续执行下去,没有被执行的则中断。此时,则不能再往线程池中添加任何任务,否则将会抛出 RejectedExecutionException 异常
  • 而 shutdownNow 为STOP,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。 它试图终止线程的方法是通过调用 Thread.interrupt() 方法来实现的,但是这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

10、提交给线程池中的任务可以被撤回吗?

可以,当向线程池提交任务时,会得到一个Future对象。这个Future对象提供了几种方法来管理任务的执行,包括取消任务。

取消任务的主要方法是Future接口中的cancel(boolean mayInterruptIfRunning)方法。这个方法尝试取消执行的任务。参数mayInterruptIfRunning指示是否允许中断正在执行的任务。如果设置为true,则表示如果任务已经开始执行,那么允许中断任务;如果设置为false,任务已经开始执行则不会被中断。

public interface Future<V> {
    // 是否取消线程的执行
    boolean cancel(boolean mayInterruptIfRunning);
    // 线程是否被取消
    boolean isCancelled();
    //线程是否执行完毕
    boolean isDone();
      // 立即获得线程返回的结果
    V get() throws InterruptedException, ExecutionException;
      // 延时时间后再获得线程返回的结果
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

取消线程池中任务的方式,代码如下,通过 future 对象的 cancel(boolean) 函数来定向取消特定的任务。

public static void main(String[] args) {
        ExecutorService service = Executors.newSingleThreadExecutor();
        Future future = service.submit(new TheradDemo());

        try {
          // 可能抛出异常
            future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
          //终止任务的执行
            future.cancel(true);
        }
 }

二、代码随想录-Spring面试题

1、Autowired和Resource区别

关键区别对比

特性@Autowired@Resource
依赖匹配方式默认按类型,可结合 @Qualifier 按名称默认按名称,未找到时按类型
注入位置字段、构造器、方法均可仅支持字段注入
处理冲突多同类型 Bean 时报错(需 @Qualifier多同类型 Bean 时需名称匹配成功
规范归属Spring 专属JSR-250 标准(Java 共有)

image.png

2、BeanFactory和FactoryBean区别

image.png

image.png

3、Spring循环依赖和解决方式

什么是循环依赖?

循环依赖是指 两个或多个 Bean 之间相互依赖 的情况,导致 Spring 容器无法完成 Bean 的初始化。例如:

// A 依赖 B
@Component
public class A {
    @Autowired
    private B b;
}

// B 依赖 A
@Component
public class B {
    @Autowired
    private A a;
}

循环依赖的类型

  1. 构造器注入循环依赖

    • 通过 new 关键字创建对象时,构造器参数必须立即满足,无法解决循环依赖。
    public class A {
        public A(B b) { ... }
    }
    public class B {
        public B(A a) { ... }
    }
    
  2. 字段/属性注入循环依赖

    • Spring 使用 @Autowired 注入字段时,允许延迟初始化(需配合 @Lazy)。

Spring 如何检测循环依赖?

  • 三级缓存机制
    Spring 在创建 Bean 时会经历以下状态:

    1. singletonFactories(单例工厂缓存)
    2. earlySingletonObjects(早期单例对象缓存)
    3. singletonObjects(最终单例对象缓存)

    当检测到循环依赖时,会抛出 BeanCurrentlyInCreationException

解决方案

方案 1:修改注入方式
  • 优先使用构造器注入(构造器注入更容易暴露循环依赖问题):

    // 错误示范(构造器注入循环依赖)
    public class A {
        public A(B b) { ... }
    }
    public class B {
        public B(A a) { ... }
    }
    
  • 改用字段注入 + @Lazy 延迟初始化

    @Component
    public class A {
        @Autowired(required = false)
        @Lazy
        private B b;
    }
    
    @Component
    public class B {
        @Autowired(required = false)
        @Lazy
        private A a;
    }
    
方案 2:调整 Bean 加载顺序
  • 使用 @Order 注解或 XML 配置指定优先级:
    @Component
    @Order(1)
    public class A { ... }
    
    @Component
    @Order(2)
    public class B { ... }
    

image.png

4、Spring中用到了哪些设计模式

image.png

image.png

5、SpringMVC中常用的注解有哪些,作用是什么

image.png

image.png

image.png