线程池(面试)

1. 线程池的简介

(1)介绍一下线程池:池化技术这种思想,包括线程池、数据库连接池、Http连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。线程池提供了一种限制和管理资源(包括执行一个任务),是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。

(2)使用线程池的好处:

  • 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

2. 线程池的实现原理

2.1 执行流程

  1. 从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下
  • 1)线程池判断核心线程池(corePoolSize)里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
  • 2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
  • 3)线程池判断线程池的最大线程(maximumPoolSize)是否已满。如果最大线程数未满,则创建maximumPoolSize-corePoolSize的救急线程来执行任务,如果maximumPoolSize已经满了,则进入下一个流程。
  • 前面都不满足,则将任务交给拒绝策略来处理这个任务。
    在这里插入图片描述
    2. ThreadPoolExecutor执行execute方法分下面4种情况。
  • 1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
  • 2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
  • 3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程(救急线程)来处理任务(注意,执行这一步骤需要获取全局锁)。
  • 4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
    在这里插入图片描述
    在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

2.2 execute源码分析流程

public void execute(Runnable command) {

    //(1) 如果任务为null,则抛出NPE异常
    if (command == null)
        throw new NullPointerException();

    //(2)获取当前线程池的状态+线程个数变量的组合值
    int c = ctl.get();

    //(3)当前线程池线程个数是否小于corePoolSize,小于则开启新线程运行(addWorker开启线程)
    if (workerCountOf(c) < corePoolSize) {
    //addWorker开启线程
        if (addWorker(command, true))
            return;
        //再次获取当前线程池的状态+线程个数变量的组合值
        c = ctl.get();
    }
/**上面return没执行,当前线程池线程个数大于corePoolSize*/

    //(4)如果线程池处于RUNNING状态,则添加任务到阻塞队列(workQueue.offer(command))
    if (isRunning(c) && workQueue.offer(command)) {

      //(4.1)二次检查获取当前线程池的状态+线程个数变量的组合值
        int recheck = ctl.get();
     //(4.2)如果当前线程池状态不是RUNNING则从队列删除任务,并执行拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);

        //(4.3)否者,如果当前线程池线程空(核心线程已执行完当前任务,空闲),则添加一个线程
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //(5)如果队列满了,则新增线程(这里是救急线程),新增失败则执行拒绝策略
    else if (!addWorker(command, false))
    //执行拒绝策略
        reject(command);
}
  • 1.代码(3)判断如果当前线程池线程个数小于 corePoolSize[workerCountOf(c) < corePoolSize],如上图会在 workers 里面新增一个核心线程(core 线程)执行该任务[addWorker(command, true)]

  • 2.如果当前线程池线程个数大于等于 corePoolSize 执行代码(4),如果当前线程池处于 RUNNING 状态则添加当前任务到任务队列,这里需要判断线程池状态是因为有可能线程池已经处于非 RUNNING 状态[c = ctl.get();],而非 RUNNING 状态下是抛弃新任务的。

  • 3.如果任务添加任务队列成功,则代码(4.2)对线程池状态进行二次校验,这是因为添加任务到任务队列后,执行代码(4.2)前有可能线程池的状态已经变化了,这里进行二次校验,如果当前线程池状态不是 RUNNING 了则把任务从任务队列移除,移除后执行拒绝策略;如果二次校验通过,则执行代码(4.3)重新判断当前线程池里面是否还有线程,如果没有则新增一个线程。

  • 4.如果代码(4)添加任务失败,则说明任务队列满了,则执行代码(5)尝试新开启线程(这里是救急线程)来执行该任务,如果当前线程池线程个数 > maximumPoolSize[!addWorker(command, false)] 则执行拒绝策略。

2.3 新增线程 addWorkder(Runnable firstTask, boolean core)

firstTask: worker线程的初始任务,可以为空
coretrue:将corePoolSize作为上限false:将maximumPoolSize作为上限
addWorker方法有4种传参的方式:
Workers Set线程池

入参方式含义
addWorker(firstTask, true)线程数小于corePoolSize时,放一个需要处理的task进Workers Set线程池。如果Workers Set线程池长度超过corePoolSize,就返回false
addWorker(firstTask, false)阻塞队列被放满时,就尝试将这个新来的task直接放入Workers Set线程池(创建救急线程),而此时Workers Set的长度限制是maximumPoolSize。如果最大线程池也满了的话就返回false
addWorker(null, false)放入一个空的task进workers Set,长度限制是maximumPoolSize。这样一个task为空的worker在线程执行的时候会去阻塞任务队列里拿任务,这样就相当于创建了一个新的线程,只是没有马上分配任务
addWorker(null, true)这个方法就是放一个null的task进Workers Set,而且是在小于corePoolSize时,如果此时线程池中的数量已经达到corePoolSize那就返回false,什么也不干。实际使用中是在prestartAllCoreThreads()方法,这个方法用来为线程池预先启动corePoolSize个worker等待从workQueue中获取任务执行
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        //(6) 检查队列是否只在必要时为空
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        //(7)循环cas增加线程个数
        for (;;) {
            int wc = workerCountOf(c);

            //(7.1)如果线程个数超限则返回false
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //(7.2)cas增加线程个数,同时只有一个线程成功
            if (compareAndIncrementWorkerCount(c))
                break retry;
            //(7.3)cas失败了,则看线程池状态是否变化了,变化则跳到外层循环重试重新获取线程池状态,否者内层循环重新cas。
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
        }
    }

    //(8)到这里说明cas成功了
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        //(8.1)创建worker
        final ReentrantLock mainLock = this.mainLock;
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {

            //(8.2)加独占锁,为了workers同步,因为可能多个线程调用了线程池的execute方法。
            mainLock.lock();
            try {

                //(8.3)重新检查线程池状态,为了避免在获取锁前调用了shutdown接口
                int c = ctl.get();
                int rs = runStateOf(c);

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    //(8.4)添加任务
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            //(8.5)添加成功则启动任务
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

3. 线程池的使用

3.1 线程池的创建

在这里插入图片描述
可以通过ThreadPoolExecutor来创建一个线程池

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

七个核心参数:

  • 1.corePoolSize (核心线程数目 (最多保留的线程数))。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
  • 2.maximumPoolSize (最大线程数目) 线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的救急线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
  • 3.keepAliveTime救急线程存活时间,针对救急线程)线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
  • 4.unit 线程活动保持时间的单位 - 针对救急线程。可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)
  • 5.workQueue (阻塞队列/任务队列) 用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
    ----❑ ArrayBlockingQueue:是一个基于数组结构有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
    ----❑ LinkedBlockingQueue:一个基于链表结构无界阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    ----❑ SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    ----❑ PriorityBlockingQueue:一个具有优先级的无界阻塞队列。
  • 6.threadFactory 线程工厂 - 可以为线程创建时起个好名字。
  • 7.handler 拒绝策略。当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。JDK提供了前4种拒绝策略:(记住)
    ----AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略。JDK
    ----CallerRunsPolicy 让调用者运行任务。JDK
    ----DiscardPolicy 放弃本次任务。JDK
    ----DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之。JDK
    其他框架提供拒绝策略:
    ----Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题。
    ----Netty 的实现,是创建一个新线程来执行任务。
    ----ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略。
    ----PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略。
    在这里插入图片描述

3.2 Executors工厂的四个线程池

Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor
在这里插入图片描述
在这里插入图片描述


3.2.1 newFixedThreadPool(固定线程池)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

执行过程:

  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
  • 当线程数达到corePoolSize=nThreads 并没有线程空闲,这时再加入任务,新加的任务会被放入LinkedBlockingQueue无界任务队列排队,直到有空闲的线程。
  • 因为最大线程数 maximumPoolSize=核心线程数,所以没有救急线程,任务队列无界可以放任意数量的任务,不会启动拒绝策略。

(1)maximumPoolSize将是一个无效参数
(2)keepAliveTime将是一个无效参数
(3)运行中的newFixedThreadPool不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution拒绝策略方法)。

特点

  • 核心线程数 == 最大线程数nThreads(0 没有救急线程被创建),因此也无需超时时间keepAliveTime
  • LinkedBlockingQueue阻塞队列是无界的,可以放任意数量的任务

评价 适用于任务量已知,相对耗时的任务


3.2.2 newCachedThreadPool(缓存线程池)

1.newCachedThreadPool是一个会根据需要创建新线程的线程池
2.newCachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但newCachedThreadPool的maximumPoolSize是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,newCachedThreadPool会不断创建新的救急线程。极端情况下,newCachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

3.核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着全部都是救急线程(60s 后可以回收) ,救急线程可以无限创建,队列采用了 SynchronousQueue 实现特点是,它没有容量没有线程来取任务,任务是放不进阻塞队列的(一手交钱、一手交货)。

4.整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况。
在这里插入图片描述
底层执行流程:

  • (1) 首先执行SynchronousQueue.offer(Runnable task)向同步队列中添加任务。如果当前maximumPool中有救急线程正在执行SynchronousQueue. poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成;否则执行(2)

  • (2) 当初始maximumPool为空,或者maximumPool中当前没有救急线程时,将没有线程执行SynchronousQueue. poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 (1) 将失败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。

  • (3) 在步骤 (2) 中新创建的线程将任务执行完后,会执行SynchronousQueue. poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1)),那么这个救急线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。

SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。newCachedThreadPool使用SynchronousQueue,把主线程提交的任务传递给空闲线程执行。

3.2.3 newSingleThreadExecutor(单个线程池)

创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
核心线程数和最大线程数都是1,意味着救急线程为0,队列使用LinkedBlockingQueue无界阻塞队列,

public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

使用场景:
希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放,还会继续执行下一次任务。

与自己创建一个单线程的区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
    ----FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
3.2.4 newScheduledThreadPool(定时任务线程池)

适用于执行延时或者周期性任务
1.创建延时任务线程池的Excutors源码,ScheduledThreadPoolExecutor实现了ScheduleExecutorService接口

2.ScheduledThreadPoolExecutor源码,ScheduledThreadPoolExecutor继承自ThreadPoolExecutor类,所以执行super构造方法调用的是ThreadPoolExecutor构造函数
在这里插入图片描述
在这里插入图片描述
整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务

ScheduledThreadPoolExecutor会把待调度的任务(ScheduledFutureTask)放到一个DelayQueue中。

DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的Scheduled-FutureTask进行排序。排序时,time小的排在前面(时间早的任务将被先执行)。如果两个ScheduledFutureTask的time相同,就比较sequenceNumber,sequenceNumber小的排在前面(也就是说,如果两个任务的执行时间相同,那么先提交的任务将被先执行)。

3.3. execute()方法和submit()方法

可以使用两个方法向线程池提交任务,分别为execute()submit()方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。

threadsPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        //TODO Auto-generated method stub
                    }
                });

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值get()方法会阻塞当前线程直到任务完成而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。(分为不超时get()和有时限等待get(long timeout,TimeUnit unit))

Future<Object> future = executor.submit(harReturnValuetask);
               try {
                        Object s = future.get();
               } catch (InterruptedException e) {
                            //处理中断异常
               } catch (ExecutionException e) {
                            //处理无法执行任务异常
               } finally {
                            //关闭线程池
                            executor.shutdown();
               }

例如:submit方法:提交任务 task,用返回值 Future 获得任务执行结果

private static void method1(ExecutorService pool) throws InterruptedException, ExecutionException {
        Future<String> future = pool.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                log.debug("running");
                Thread.sleep(1000);
                return "ok";
            }
        });

        log.debug("{}", future.get());
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答:Java线程池是Java中的一个重点知识,并且在Java的工作中经常会遇到,因此在面试中也是必问的面试题目。以下是一些常见的Java线程池面试题: 1. 谈谈什么是线程池? 2. 为什么要使用线程池? 3. 你们哪些地方会使用到线程池? 4. 线程池有哪些作用? 5. 线程池的创建方式有哪些? 6. 线程池底层是如何实现复用的? 7. ThreadPoolExecutor核心参数有哪些? 8. 线程池创建的线程会一直在运行状态吗? 9. 为什么阿里巴巴不建议使用Executors? 10. 线程池的底层实现原理是什么? 11. 线程池队列满了,任务会丢失吗? 12. 线程池的拒绝策略类型有哪些? 13. 线程池如何合理配置参数? 这些问题涵盖了线程池的基本概念、使用场景、实现原理以及相关的配置和策略等方面的知识。了解这些问题能够帮助面试者更好地理解和应用Java线程池。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [java线程池面试题有哪些?java线程池常见面试题](https://blog.csdn.net/muli525/article/details/123553744)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *3* [(一)【Java精选面试题】线程池底层实现原理(含答案)](https://blog.csdn.net/qq_30999361/article/details/124924343)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值