# Java面试
# 多线程
# Java中有三种方式创建线程,分别是?
# 直接继承Thread类创建线程
通过继承Thread类,重写run()方法,创建线程对象并执行start()方法来创建线程,这种方式没有任何优点,缺点是线程类无法再继承其他对象,实际项目里禁止使用这种方式创建线程。
package com.kieoo.interview;
/**
* 通过继承Thread类来实现线程,重写的是run()方法而不是start()方法,同时
* 因为Java中的类是单继承,以这种方式实现的线程就不支持继承其他类了
*
* @author xuhaodi
*/
public class MyThread extends Thread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
@Override
public void run() {
System.out.println("MyThread execute");
}
}

# 实现Runnable接口创建线程
通过实现Runnable接口,实现run()方法的方式来创建线程,推荐使用。
package com.kieoo.interview;
/**
* 实现Runnable接口及接口中的run()方法,通过
* 创建new Thread()对象并在构造器中提供当前类实例的方式创建线程。
*
* @author xuhaodi
*/
public class MyThread implements Runnable {
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.start();
}
@Override
public void run() {
System.out.println("MyThread execute");
}
}

# 实现Callable接口创建线程
Callable接口和Runnable的区别是,Runnable接口里线程异步执行,当前线程不需要获取子线程的结果,而Callable接口配合FutureTask使用,FutureTask会阻塞当前线程,最终获取到Runnable线程(子线程)的返回结果。
package com.kieoo.interview;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 实现Callabe接口及接口中的call()方法,同时结合使用FutureTask类获取线程的执行结果
*
* @author xuhaodi
*/
public class MyThread implements Callable<String> {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new MyThread());
Thread thread = new Thread(futureTask);
thread.start();
String result = futureTask.get();
System.out.println(result);
}
public String call() {
return "MyThread execute";
}
}

# 为什么不建议使用Executors来创建线程池?不用Executors那要用什么呢?
# 为什么不建议使用Executors来创建线程池?
Executors是java提供的一个线程池工具类,这个工具类提供了很多方便易用的线程池模型,如固定线程池(FixedThreadPool)、单线程池(SingleThreadExecutor)、缓存线程池(CachedThreadPool)、定时线程池(ScheduledThreadPool)。有利就有弊,这些工具类在方便我们的同时也隐藏了一些细节(底层数据结构的队列默认长度是Integer.MAX_VALUE),在项目投产运行过程中线上经常会出现线程池导致的OOM问题或CPU过载问题,因此不推荐使用。
FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
# 不用Executors那要用什么线程池呢?
在不使用Executors的情况下,我们可以使用java提供的另一个线程池工具ThreadPoolExecutor,使用ThreadPoolExecutor需要我们设置corePoolSize:核心线程数,maximumPoolSize:最大线程数,workQueue:工作队列,handler:拒绝策略等配置,因此使用ThreadPoolExecutor不容易出现线程池相关问题。
考虑到我们的开发生态是Spring,推荐使用Spring提供的ThreadPoolTaskExecutor线程池,这个线程池是基于ThreadPoolExecutor实现的,在Spring容器中以单例Bean存在,可以很好的与@Async、事务管理等 Spring 特性配合使用时。
补充说明
1.在我目前所负责开发的项目中,由于项目本身是微服务架构,且已经迭代到了v7版本,存在Executors、ThreadPoolExecutor、ThreadPoolTaskExecutor混用的情况。
# 线程池有哪些关键参数?如何设置这些参数呢?
# 关键参数
名称定义
线程池的名称对问题定位至关重要,必须要为线程池指定线程名称。核心线程数
corePoolSize:核心线程数,即使线程空闲也不会被回收,除非设置allowCoreThreadTimeOut。最大线程数
maximumPoolSize:最大线程数,当工作队列满了之后,可以创建的最大线程数。拒绝策略
handler:拒绝策略,当线程池和队列都满了之后,如何处理新提交的任务。
# 设置参数的核心原则
明确配置所有参数,使用有界队列,设置合理的拒绝策略,并给线程命名以便监控和排查问题。
# 线程池有四种拒绝策略,分别是?
# 直接报错
AbortPolicy(默认策略):抛出异常,拒绝新任务
# 由调用者线程执行任务
CallerRunsPolicy:调度者线程相当于线程池里负责协调任务的线程,由它负责执行新的任务的话,会降低新任务提交速度,起到削峰填谷作用。生产环境推荐使用。
# 静默丢弃新任务
DiscardPolicy:直接丢弃新任务,不抛异常,无任何通知
# 丢弃队列中最旧的任务
DiscardOldestPolicy:丢弃队列中最旧的任务
# 线程池有几种状态?分别是如何变化的?
线程池有五种状态,分别是:
| 状态 | 说明 |
|---|---|
| RUNNING | 表示线程池正常运行,会接收新任务并且会处理队列中的任务 |
| SHUTDOWN | 不会接收新任务并且会处理队列中的任务,任务处理完会中断所有线程 |
| STOP | 不会接收新任务并且不会处理队列中的任务,并且会直接中断所有线程 |
| TIDYING | 所有线程都停止了之后,线程池的状态就会变成TIDYING,一旦到达此状态,就会调用线程池的terminated()方法,这个方法是一个空方法,留给程序员进行扩展。 |
| TERMINATED | terminated()方法执行完之后状态就会变成TERMINATED |
这五种状态不能随意转换,只会有以下几种转换情况:
| 转换前 | 转换后 | 转换条件 |
|---|---|---|
| RUNNING | SHUTDOWN | 手动调用shutdown()方法触发 |
| RUNNING | STOP | 手动调用shutdownNow()方法触发 |
| SHUTDOWN | TIDYING | 线程池中所有线程都停止后自动触发 |
| STOP | TIDYING | 线程池中所有线程都停止后自动触发 |
| TIDYING | TERMINATED | 线程池自动调用terminated()方法后触发 |
线程池状态转换图如下:

# 线程池中提交一个任务的流程?
1)使用execute()方法提交Runnable对象
2)判断线程池中当前线程数是否小于corePoolSize核心线程数
3)如果小于,创建新的核心线程并执行Runnable
4)如果大于等于,则尝试将Runnable加入到workQueue工作队列中
5)如果workQueue没满,则将Runnable正常入队,等待执行
6)如果workQueue满了,则会入队失败,线程池会继续尝试增加线程
7)判断当前线程池中的线程数是否小于maxinumPoolSize
8)如果小于,则创建新线程并执行任务
9)如果大于等于,则执行拒绝策略,拒绝此Runnable
对应的流程图如下:

# 线程池中核心线程数,最大线程数如何设置?
线程池负责执行的任务可以分为三种类型,分别是CPU密集型任务,IO密集型任务以及混合型任务。
# 设置核心线程数
- CPU密集型任务
特点:这类任务主要消耗CPU资源,很少进行I/O操作,如复杂的计算任务。
设置策略:线程池大小建议设置为CPU核心数+1(可以通过Runtime.getRuntime().availableProcessors();获取CPU核心数)。因为对于CPU密集型任务,增加线程数量并不能提高执行效率,反而可能导致线程上下文切换的额外开销,降低系统性能。CPU核心数+1的策略可以在多核CPU上尽可能利用CPU资源的同时,保留一定余地处理系统任务调度。
- CPU密集型任务
- IO密集型任务
特点:这类任务执行过程中,大部分时间都在等待I/O操作完成,如文件读写、网络通信。
设置策略:
方法一:推荐线程池大小设置为CPU核心数*2。由于I/O操作不占用CPU,增加线程可以让CPU在等待I/O时处理其他任务,提升CPU利用率。
方法二:更精细的计算方法是根据线程CPU运行时间和等待时间的比例来确定。公式为:((CPU时间占比 + 等待时间占比) / CPU时间占比) * CPU核心数。例如,如果每个线程CPU运行0.5秒,I/O等待1.5秒,那么线程数为((0.5+1.5)/0.5)*8=32。简化公式为:最佳线程数 = (等待时间与CPU时间比 + 1) * CPU核心数。
- IO密集型任务
- 混合型任务
特点:既包含CPU密集操作也包含I/O操作。
设置策略:针对这种情况,较为理想的做法是将任务拆分为CPU密集型和IO密集型,分别使用专门的线程池处理。这样可以根据各自的特点,按照上述原则分别设置合适的线程数。如果拆分困难,可以评估任务中CPU和I/O操作的比例,折中选取一个相对平衡的线程池大小。
- 混合型任务
# 设置最大线程数
最大线程数一般设置为核心线程数的2~3倍,这个数值是不固定的,可以通过压测等操作来确定合适的最大线程数。
补充说明-实际项目中如何设置
1.在我负责的项目中,有两个微服务使用了Spring的ThreadPoolTaskExecutor线程池,他们分别是如下设置:
// 核心线程=CPU核心数+1
// 最大线程数=核心线程数*2
@Bean
public Executor convertPool(){
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(5);
threadPoolTaskExecutor.setMaxPoolSize(10);
threadPoolTaskExecutor.setQueueCapacity(99999);
threadPoolTaskExecutor.setThreadNamePrefix("file-convert-");
// 拒绝策略-由调度者线程负责执行该任务
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
// 核心线程数=动态获取的CPU核心数
// 最大线程数=核心线程数*2
@Bean
public ThreadPoolTaskExecutor threadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
executor.setQueueCapacity(1000);
executor.setKeepAliveSeconds(300);
// 拒绝策略:默认策略-抛出异常
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
# Synchronized同步关键字和ReentrantLock的区别?
| Synchronized同步关键字 | ReentrantLock可重入锁 |
|---|---|
| Java中的一个关键字 | JDK提供的一个类 |
| 自动加锁与释放锁 | 需要手动加锁和释放锁 |
| JVM层面的锁 | API层面的锁 |
| 非公平锁 | 公平锁/非公平锁 |
| 锁的是当前对象,锁信息保存在对象头中 | int类型的state标识来标识锁的状态 |
| 底层有锁升级过程 | 没有锁升级过程 |
项目中哪些地方用到了synchronized
看了下项目,没用到,所以自己写一个
public class Test {
private String node;
// 在方法上使用,锁当前实例对象
public synchronized void method(){
// 在代码块使用,锁当前实例对象
synchronized (this){
node++;
}
}
// 在静态方法使用,锁类对象Test.class
public synchronized static void staticMethod(){
// 在代码块中使用,锁类对象Test.class
synchronized (Test.class){
node++;
}
}
}
项目中哪些地方用到了ReentrantLock
项目中在消息队列消费消息时使用了可重入锁
@Override
public void listen(String message, String topic, String tag) {
try {
lock.lock();
logger.info("service-point::::::::::接收到MQ消息topic={}, tags={}, 消息内容={}", topic, tag, message);
//部门相关操作事件
if ("department".equals(topic)) {
departmentConsume.consume(message, topic, tag);
}
/**
* tag:createPointConfig||createPointItem
*
*/
if ("point".equals(topic)) {
if ("initRootPointConfigAndItem".equals(tag)) {
pointConsume.consume(message, topic, tag);
}
if ("addPoints".equals(tag)) {
pointConsume.consumeAddPoints(message, topic, tag);
}
//2-扣除,3-返还,4-增加 悬赏分
if ("deductionOrReturn".equals(tag)) {
pointConsume.deductionOrReturn(message, topic, tag);
}
}
} finally {
lock.unlock();
}
}
# ThreaLocal有哪些使用场景?
ThreadLocal底层会将ThreadLocal数据存储在线程栈内存中,当前线程可以在任意时刻,任意方法中获取ThreadLocal中的信息。
仅在当前线程中会使用的数据,可以放在ThreadLocal中,如用户信息,线程日志每个线程中唯一,且不会在不同线程间共享,就可以存储在ThreadLocal中。
补充说明
在我参与的项目中,在日志中使用了ThreadLocal,以及项目使用的框架SA-Token默认的用户信息存储机制中,会通过过滤器解析Token,将用户信息存储到线程本地存储中(好处之一是可以解决参数透传问题)。
此外,数据库连接等信息也是非常适合存入ThreadLocal中的。
# AQS相关
# 什么是AQS,简单说一下
AQS(Abstract Queued Synchronizer 抽象队列同步器),是Java并发包中构建锁和同步器的核心框架,像ReentrantLock、CountDownLatch等常用同步工具都是基于它实现的。
# AQS的核心思想
AQS使用一个经volatile修饰的int类型的state变量来表示同步状态,同时保证同步状态在多线程环境下的可见性,通过CAS操作state,保证针对state的复合操作具有原子性。此外,针对未获取同步器的线程,使用一个FIFO队列来管理获取同步状态失败的线程。避免线程的自旋等待以及阻塞。
# ReentrantLock的非公平锁如何实现?
// 非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 1. 尝试把state从0改成1(获取锁),能获取到直接使用资源
* 2. 获取不到锁的话执行acquire()方法,这个方法又会执行tryAcquire()方法,最终执行父类的nonfairTryAcquire()方法
* 3. nonfairTryAcquire方法判断锁是否释放,如果释放,同样使用CAS加锁,如果没释放,判断当前线程是否是加锁的线程,是的话锁的次数+1
* 4. 步骤3也获取不到锁,将当前线程放到队列中排队
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

# ReentrantLock的公平锁如何实现?
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

# 综合
# Cookie和Session的区别?
Cookie和Session在定义、存储位置、安全性、应用场景方面均存在显著差异。Cookie存储在浏览器,Session存储在服务器,应用通过在Cookie里传SessionId对请求进行认证。
定义不同
Cookie属于HTTP协议,它是一种存储在用户本地终端(如电脑、手机等)上的小型文本文件,用于辨别用户身份和进行Session跟踪。它允许服务器在客户端上存储少量数据,并在后续的请求中检索这些数据。
Session(会话)是服务器为了区分和跟踪不同用户而创建的一个服务器端状态。由于HTTP协议本身是无状态的,Session机制就是为了解决这个问题而诞生的。Session用于存储和管理用户会话相关的数据。每个用户都会被分配一个唯一的Session ID,该ID通过Cookie或URL重写的方式发送给客户端浏览器,并在后续的存储位置不同
Cookie存储在客户端用户浏览器的本地磁盘中,Session数据存放在服务器的内存中,但Session ID通常通过Cookie或URL重写的方式传递给客户端。安全性差异
Cookie存储在客户端,因此存在被分析或篡改的风险,安全性较低。Session存储在服务器端,对客户端不可见,因此安全性较高。应用场景不同
Cookie常用于记录用户偏好、登录状态等。Session常用于用户身份认证、购物车功能等。
# Session和Token的区别?
Session和Token的存在都是为了解决HTTP本身的无状态问题。两者在存储位置,状态管理,数据结构,生命周期等方面都有区别。
| 特性 | Session | Token (以JWT为例) |
|---|---|---|
| 存储位置 | 服务器端(内存、Redis等) | 客户端(浏览器的LocalStorage、Cookie等) |
| 状态管理 | 有状态的 | 无状态的 |
| 数据结构 | 只是一个ID,实际数据在服务器 | 自包含的(包含用户信息、过期时间等) |
| 扩展性 | 需要Session共享机制 | 天然支持分布式 |
| 跨域支持 | 需要额外配置 | 天然支持 |
| 生命周期 | 服务器端控制 | Token自身控制(通过过期时间) |