一、小林-Java并发面试题
1、volatile可以保证线程安全吗?
volatile关键字可以保证可见性,但不能保证原子性,因此不能完全保证线程安全。volatile关键字用于修饰变量,当一个线程修改了volatile修饰的变量的值,其他线程能够立即看到最新的值,从而避免了线程之间的数据不一致。
但是,volatile并不能解决多线程并发下的复合操作问题,比如i++这种操作不是原子操作,如果多个线程同时对i进行自增操作,volatile不能保证线程安全。对于复合操作,需要使用synchronized关键字或者Lock来保证原子性和线程安全。
2、非公平锁吞吐量为什么比公平锁大?
- 公平锁执行流程:获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
- 非公平锁执行流程:当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。
3、介绍一下线程池的工作原理
线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗,线程池的工作原理如下图:
线程池分为核心线程池,线程池的最大容量,还有等待任务的队列,提交一个任务,如果核心线程没有满,就创建一个线程,如果满了,就是会加入等待队列,如果等待队列满了,就会增加线程,如果达到最大线程数量,如果都达到最大线程数量,就会按照一些丢弃的策略进行处理。
任务执行流程如下:
提交任务 → 核心线程是否已满?
├─ 未满 → 创建核心线程执行
└─ 已满 → 任务入队
├─ 队列未满 → 等待执行
└─ 队列已满 → 创建非核心线程
├─ 未达最大线程数 → 执行任务
└─ 已达最大线程数 → 执行拒绝策略
4、线程池的参数有哪些?
线程池的构造函数有7个参数:
- 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,则会创建线程来执行线程池的任务。
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 共有) |
2、BeanFactory和FactoryBean区别
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;
}
循环依赖的类型
-
构造器注入循环依赖
- 通过
new关键字创建对象时,构造器参数必须立即满足,无法解决循环依赖。
public class A { public A(B b) { ... } } public class B { public B(A a) { ... } } - 通过
-
字段/属性注入循环依赖
- Spring 使用
@Autowired注入字段时,允许延迟初始化(需配合@Lazy)。
- Spring 使用
Spring 如何检测循环依赖?
-
三级缓存机制:
Spring 在创建 Bean 时会经历以下状态:- singletonFactories(单例工厂缓存)
- earlySingletonObjects(早期单例对象缓存)
- 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 { ... }