# 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密集型任务以及混合型任务。

# 设置核心线程数

    1. CPU密集型任务
      特点:这类任务主要消耗CPU资源,很少进行I/O操作,如复杂的计算任务。
      设置策略:线程池大小建议设置为CPU核心数+1(可以通过Runtime.getRuntime().availableProcessors();获取CPU核心数)。因为对于CPU密集型任务,增加线程数量并不能提高执行效率,反而可能导致线程上下文切换的额外开销,降低系统性能。CPU核心数+1的策略可以在多核CPU上尽可能利用CPU资源的同时,保留一定余地处理系统任务调度。
    1. 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核心数。
    1. 混合型任务
      特点:既包含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自身控制(通过过期时间)