线程池基础

Executor接口:定义execute()

线程池的初衷是屏蔽线程的创建,线程复用等特点,基于这个设计思想,JDK设计了Executor这个顶级接口:
在这里插入图片描述
这个接口只有一个抽象方法。

ExecutorService接口:引入线程池状态+Future

Executor顶级接口就一个方法,过于单薄,ExecutorService继承Executor接口,引入了一些新的方法:
在这里插入图片描述
ExecutorService比较与父类Executor的特点:
1、引入了线程池的状态:要求子类必须实现诸如shutDown()、shutDownNow()、isTerminated()、isShutDown()等方法以便反馈线程池的状态:已关闭?任务已结束?
2、引入submit()方法,它的灵感来自于Executor的execute(),但execute()是没有返回值的,而submit()会返回一个Future对象(这玩意是FutureTask的爸爸),也就是说,从ExecutorService开始,线程池开始有返回值了。

ScheduledExecutorService接口:定时任务

ScheduledExecutorService在ExecutorService的基础上提出了定时任务池的概念。
在这里插入图片描述ScheduledExecutorService要求子类实现定时方法,能够周期性地执行任务。与submit()类似,当任务提交给这些定时方法后,会返回ScheduledFuture

AbstractExecutorService抽象类

目前为止,我们看到的都是JDK中关于线程池的接口,它们之间的关系是这样的:
在这里插入图片描述
这些接口只是规定了一些方法,并没有具体的实现,所以JDK提供了AbstractExecutorService抽象类:
在这里插入图片描述
AbstractExecutorService主要做了两件事:
1、新增了newTaskFor()方法,把runnable和callable任务封装成了FutureTask类型
2、对ExecutorService接口中submit()方法进行了实现了
在这里插入图片描述
在这里插入图片描述
newTaskFor()就是把runnable和callable任务封装成FutureTask类型。
为什么AbstractExecutorService为什么要引入FutureTask呢,因为线程池的顶级接口只能执行Runnable,引入FutureTask,可以兼容callable。
所以,线程池为什么要引入FutureTask包装Runnable和Callable?
● Executor顶级接口的历史原因,需要包装Runnable和Callable,统一返回值问题
● 谋求更强大的功能(异步+结果)

ForkJoinPool&ThreadPoolExecutor

JDK的线程池设计发展到这一步,终于出现了两个划时代的实现类:
在这里插入图片描述
主要说一下ThreadPoolExecutor实现类:
ThreadPoolExecutor的贡献有以下几点:
1、 首先引入corePoolSize、maximumPoolSize、keepAliveTime、ThreadFactory、RejectHandler等参数配置,真正实现了线程的“池化”。线程池里的线程数、队列长度、拒绝策略会在这几个阈值的影响下动态调整,大大提高了执行效率和复用率。
在这里插入图片描述

2、抽象出workers(线程池),一个Worker就是一个线程
在这里插入图片描述

3、抽象出workQueue(阻塞队列),缓冲待执行任务
在这里插入图片描述

4、内置4种拒绝策略,规避执行压力
在这里插入图片描述
在了解ThreadPoolExecutor之前需要先了解一些其他的东西:

池化技术

比如数据库连接池、常量池、线程池、对象池等等。池化技术是计算机世界里比较常用的、行之有效的优化手段。线程池中的“池”,到底指代什么?
我们向Executor提交任务,Executor自己维护Thread,这个"池"就是Thread集合。
和数据库连接池不同的是:我们通过数据库连接池可以取出一个connection,执行sql语句后,使用close()方法返回connection;但是你不可能从线程池中获取一个线程的,而是把要执行的任务丢到线程池中。

生产消费模型

如果往线程池不断提交任务,大致会经历4个阶段:
● 核心线程处理任务
● 任务进入任务队列等待
● 非核心线程处理任务
● 拒绝策略
特别是第二个阶段,来不及处理的任务会被暂存入workQueue(任务队列),于是典型的生产消费模型就出现了。

几个重要概念

线程池如何复用线程?

先不管如何复用线程,先想一下如何回收/销毁线程?

“线程”这个词,其实有两个层次的指代:Thread对象、JVM线程资源(本质还是操作系统线程)。Thread对象与线程资源之间是绑定关系,一个线程资源被分配后,会找到Thread#run()作为代码的执行入口。

线程什么时候销毁呢?正常来说,new Thread(tartget).start()后,操作系统就会分配线程资源,而等到线程执行完Thread#run()中的代码,就会自然消亡。至于Thread对象,如果没有引用,也会被GC回收。

看到这里你可能就明白了,只要任务永远不结束,线程就永远死不了。任务如何才能永远不结束呀?要么循环做任务、要么阻塞。

线程池本质也是Thread,只是单体和集合的区别。既然Thread“跑完任务就销毁”的特性是天生的、注定的,线程池也无法改变这一点。所以,线程池要想让内部线程一直存活着,就要keeps threads busy working,也就是让它们一直干活。实在没活干怎么办?那就阻塞着呗(可以用阻塞队列)!总之,不能让你“执行完毕”,否则就销毁了。

如何保证只销毁“非核心线程”

简单粗暴:
● 当前线程数 <= corePoolSize,那么所有线程都是核心线程,不回收
● 当前线程数 > corePoolSize,回收多余线程

超过corePoolSize后为什么优先入队?

首先,入队的好处是缓冲执行任务,队列满了之后才扩展线程执行任务;其次,线程资源比较宝贵,用队列缓冲的话,就别用额外创建线程。

简单版TheadPool

public class SimpleThreadPool {

    //任务队列,线程超过核心线程数,会把任务放到阻塞队列中
    BlockingQueue<Runnable> workQueue;
    //工作线程
    List<Worker> workers = new ArrayList();

    //构造器
    SimpleThreadPool(int poolSize,BlockingQueue<Runnable> workQueue){
        this.workQueue = workQueue;
        //创建线程,并加入到线程池
        for (int i = 0; i < poolSize; i++) {
            Worker worker = new Worker();
            worker.start();
            workers.add(worker);
        }

    }

    /**
     * 提交任务
     */
    public void execute(Runnable task){
        // 任务队列满了则阻塞
        try {
            workQueue.put(task);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }



    /**
     * 工作线程,负责执行任务
     */
    class Worker extends Thread{
        @Override
        public void run() {
            //循环获取任务,如果任务为空则阻塞,线程不会销毁
            while (true){
                try {
                    Runnable task = workQueue.take();
                    task.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

测试

public class SimpleThreadPoolTest {
    public static void main(String[] args) {
        SimpleThreadPool threadPool = new SimpleThreadPool(2, new ArrayBlockingQueue<Runnable>(2));

        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("第一个任务开始");
                    sleep(3);
                System.out.println("第一个任务结束");
            }
        });

        threadPool.execute(()->{
            System.out.println("第二个任务开始");
            sleep(3);
            System.out.println("第二个任务结束");
        });
        threadPool.execute(()->{
            System.out.println("第三个任务开始");
            sleep(3);
            System.out.println("第三个任务结束");
        });
    }

    private static void sleep(int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

复杂版ThreadPool

public class ThreadPool {

    private final ReentrantLock mainLock = new ReentrantLock();

    /**
     * 工作线程
     */
    private final List<Worker> workers = new ArrayList<>();
    /**
     * 任务队列
     */
    private  BlockingQueue<Runnable> workQueue ;

    /**
     * 核心线程数
     */
    private final int corePoolSize;

    /**
     * 最大线程数
     */
    private final  int maxPoolsize;
    /**
     * 非核心线程空闲时间
     */
    private final long keepAliveTime;

    public ThreadPool(int corePoolSize,
                      int maxPoolsize,
                      long keepAliveTime,
                      TimeUnit timeUnit,
                      BlockingQueue<Runnable> workQueue){
        this.corePoolSize=corePoolSize;
        this.maxPoolsize = maxPoolsize;
        this.keepAliveTime = timeUnit.toNanos(keepAliveTime);
        this.workQueue = workQueue;

    }

    /**
     * 提交任务
     */
    public void execute(Runnable task){
        //参数为空,抛错
        Assert.notNull(task,"task is null");
        //创建核心线程,处理任务
        if (workers.size()<corePoolSize){
            this.addWork(task,true);
            return;
        }
        //尝试加入任务队列
        boolean offer = workQueue.offer(task);
        if (offer){
            return;
        }
        //任务队列也满了之后,创建非核心线程处理任务
        if (!this.addWork(task,false)){
            // 非核心线程数达到上限,触发拒绝策略
            throw new RuntimeException("拒绝策略");
        }
    }

    public boolean addWork(Runnable task,boolean flag){
        int size = workers.size();
        //如果当前线程数大于等于核心线程数 或者创建非核心线程时,线程数大于等于最大线程数 返回false
        if (size>=(flag?corePoolSize:maxPoolsize)){
            return false;
        }

        boolean workerStarted = false;

        try {
            Worker worker = new Worker(task);
            final  Thread thread = worker.getThread();
            if (thread!=null){
                mainLock.lock();
                workers.add(worker);
                thread.start();
                workerStarted = true;
            }
        } finally {
            mainLock.unlock();
        }

        return workerStarted;
    }

    private void runWorker(Worker worker){
        Runnable task = worker.getTask();
        try {
            //循环处理任务
            while (task!=null || (task = getTask()) != null){
                task.run();
                task = null;
            }
        } finally {
            /**
             * 从循环中退出来,也就意味着当前线程不是核心线程,需要销毁
             */
            workers.remove(worker);

        }
    }

    private  Runnable getTask(){
        boolean TimeOut = false;

        // 循环获取任务
        for (;;){
            //当前线程数超过核心线程数,返回null
            boolean timed = workers.size()>corePoolSize;
            if (timed && TimeOut){
                return null;
            }

            try {
                // 是否需要检测超时
                // 1.需要:poll阻塞获取,等待keepAliveTime,等待结束就返回,不管有没有获取到任务
                // 2.不需要:take持续阻塞,直到获取结果
                Runnable task=   timed ? workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS) : workQueue.take();
                if (task!=null){
                    return task;

                }
                TimeOut = true;
            } catch (InterruptedException e) {
               TimeOut = false;
            }
        }



    }

    @Getter
    @Setter
    private class Worker implements Runnable{

        private Thread thread;
        private Runnable task;

        public Worker(Runnable task){
            this.task=task;
            thread=new Thread(this);
        }

        @Override
        public void run() {
            runWorker(this);
        }
    }


}

测试

public class ThreadPoolTest {
    public static void main(String[] args) {
        //创建线程池
        /**
         *核心线程1个 , 最大线程2个, ‘=
         * 提交4个任务,第一个任务交给核心线程,第二个进入任务队列,第三个任务交给非核心线程,第四个被拒绝
         */
        ThreadPool threadPool = new ThreadPool(1, 2, 1,TimeUnit.SECONDS, new ArrayBlockingQueue(2));



        threadPool.execute(new FutureTask<String>(new Runnable() {
            @Override
            public void run() {
                System.out.println();
            }
        },"success"));


        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("{}:执行第1个任务..."+ Thread.currentThread().getName());
                sleep(10);
            }
        });
        sleep(1);

        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("{}:执行第2个任务..."+ Thread.currentThread().getName());
                sleep(10);
            }
        });
        sleep(1);

        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("{}:执行第3个任务..."+ Thread.currentThread().getName());
                sleep(10);
            }
        });

        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("{}:执行第4个任务..."+ Thread.currentThread().getName());
                sleep(10);
            }
        });
        sleep(1);

        System.out.println("main结束");
    }
    private static void sleep(int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ThreadPoolExecutor源码分析

实际工作中,Executor、ExecutorService、AbstractExecutorService并不常见,我们一般直接使用ThreadPoolExecutor这个实现类。通常情况下,大家讨论线程池时,其实都是在讨论ThreadPoolExecutor,只不过他们自己都没意识到。

上面说到,ExecutorService接口新增了submit(),可以接收Runnable/Callable,并且支持返回值。而AbstractExecutorService则“初步”实现了submit():
在这里插入图片描述

submit()仅仅做了Runnable/Callable的统一包装,具体的任务执行还是交给Executor#execute()。也就是通过模板方法模式,把具体的实现交给了子类。而这个“子类”,一般就是指ThreadPoolExecutor:
在这里插入图片描述
execute()本身思路很明确,它安排了任务处理的总流程:
● 优先使用核心线程处理任务
● 核心线程满了,优先入队
● 等到队列满了,尝试开启非核心线程处理任务
● 如果非核心线程也满了,那就没办法了,执行拒绝策略

核心线程是如何处理任务的?

● 优先使用核心线程处理任务
● 核心线程满了,优先入队
● 等到队列满了,尝试开启非核心线程处理任务
● 如果非核心线程也满了,那就没办法了,执行拒绝策略

public void execute(Runnable command) { // 这里的command,就是submit()里封装的FutureTask
    if (command == null)
        throw new NullPointerException();
    
    //(1)获取ctl。ctl用来标记线程池状态(高3位),线程个数(低29位)
    int c = ctl.get();

    //(2)当前线程池线程个数是否小于corePoolSize,小于则【创建核心线程】处理当前任务。
    if (workerCountOf(c) < corePoolSize) {
        // addWorker()第二个参数代表是否核心线程
        if (addWorker(command, true))
            // 注意,核心线程的创建、执行都在addWorker()方法中,一旦任务提交给核心线程,整个execute()就结束了
            return;
        c = ctl.get();
    }
    
    // ...
}

addWordker()方法创建线程:
● 先把task封装成Worker
● 经过一系列步骤,启动Worker里的Thread,线程开启后会执行Worker里的Task
在这里插入图片描述
一起来看看Worker是啥:
在这里插入图片描述
看上面Worker的构造器,我们不难发现 Worker = Thread + Task:
● getThreadFactory().new Thread()会创建一个线程
● Task则是Executor#execute(task)提交的那个任务
调用的流程大致是这样的:
在这里插入图片描述
右图Worker#run()又调用ThreadPoolExecutor#runWorker():

在这里插入图片描述
整个的调用链
在这里插入图片描述

线程池的生产消费模型

核心线程的处理逻辑虽然绕,但理清楚以后还是简单的。但从上面的介绍来看,你会发现和普通的new Thread(target).start()没太大区别,不是说线程池本质是生产消费模型吗?ThreadPoolExecutor的生产者是谁,消费者又是谁,什么时候消费、如何消费?

前面提到过,如果不断的往线程池里面添加任务,大概会经历4个阶段:
1.核心线程处理任务
2.核心线程满了,任务进入任务队列
3.任务队列满了,扩展非核心线程处理任务
4.拒绝策略
任务进入队列即为生产,而当核心线程/非核心线程处理完手头的任务,从workQueue中取出任务,就是消费。
生产消费模式是通过阻塞队列实现的,我们可以在创建ThreadPoolExecutor的时候指定阻塞队列:
在这里插入图片描述

生产者

我们往线程池提交任务的过程就是生产的过程:

public void execute(Runnable command) { // 这里的command,就是submit()里封装的FutureTask
    if (command == null)
        throw new NullPointerException();
    
    int c = ctl.get();

    // 假设核心线程数已满,跳过这一步
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    // workQueue.offer(command):尝试把任务加入到workQueue(任务队列),这个workQueue就是new ThreadPoolExecutor()时指定的BlockingQueue
    if (isRunning(c) && workQueue.offer(command)) {
        // ...
    }
    
    else if (!addWorker(command, false))
        reject(command);
}

任务进来后,先判断核心线程是否满了,如果没有满,通过new Woker()的方法创建核心线程,调用线程的start()的方法,会自动调用run()方法,而Worker重写了run()方法,在run()方法中调用runWorker()方法,此方法用来循环执行任务,并通过调用getTask()方法循环从阻塞队列中取出任务,如果没有任务,队列就会阻塞,直到任务重新进入队列。

消费者

核心/非核心线程处理完手头任务后,是如何从任务队列获取新任务的呢?还记得ThreadPoolExecutor#runWorker()吗,异步线程开启后最终会调用它。

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    // ...
    try {
        // 线程开启后,进入循环:当前任务不为空 || 队列任务不为空
        while (task != null || (task = getTask()) != null) {
            // ...
            try {
                // ...
                task.run();
                // ...
            } finally {
                // ...
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

addWorker()可不是简单地创建线程并执行当前任务就完事了,runWoker()内部会循环,看看队列里有没有任务需要处理(getTask).

线程的复用与销毁

一般来说new Thread().start()执行完目标任务后,就会自然销毁。那么线程池是如何做到任务跑完了之后线程不销毁的呢?
答案就在getTask()方法中:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    // 尝试循环获取任务
    for (;;) {
        // 看不懂,跳过
        int c = ctl.get();
        int rs = runStateOf(c);

        // 看不懂,跳过 Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        // 看不懂,跳过
        int wc = workerCountOf(c);

        // 是否需要检测超时:允许所有线程超时回收(包括核心线程) || 当前线程数超过核心线程
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;


        // 超时了,跳出循环
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // 是否需要检测超时
            // 1.需要:poll阻塞获取,等待keepAliveTime,等待结束就返回,不管有没有获取到任务
            // 2.不需要:一直阻塞,直到获取结果
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            
            // r==null,任务为空,timedOut=true
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

也就是说,如果是核心线程,timed永远为false,那么就会调用workQueue.take()一直阻塞下去,直到有新的任务提交进来。但是处理结束后,还是会进入循环,周而复始。由于线程永远处于阻塞等待任务、执行任务、继续阻塞等待任务的死循环中,也就永远不会销毁了。

线程池之所以能复用,仅仅是让线程进入阻塞状态罢了。

销毁

特别说明一下:
核心线程不会被销毁,并不是指的是特定的线程不会被销毁,而是线程无论怎么销毁,最终会保持线程池中的线程数不小于核心线程数;
而所说的线程销毁,就是任务执行完成之后,继续往下走,和new Thread().start()一样了。

如果不考虑allowCoreThreadTimeOut(一般不会刻意设置为true):
● 那么当线程数不超过corePoolSize时,每一个线程都是核心线程,此时并不需要进行“超时检测”,所以线程会直接调用BlockingQueue#take()阻塞等待,直到有新的任务被提交。即使跳出getTask(),回到runWorker()执行完新的任务,也别指望线程就这么结束了,因为runWorker()本身也是循环,又会回到getTask()…从宏观上来看,就形成了所谓的“线程池Thread复用”
● 当前线程数超过corePoolSize,那么就会getTask()里的循环就会进行“超时检测”。所谓的超时检测,其实就是“阻塞等待keepAliveTime”,等待结束直接返回,不论是否拿到任务。假设任务为null,就会跳过if判断,设置timeOut=true(线程当前数超过了corePoolSize && 这次又取不到任务,说明啥?线程池没任务,空闲了,所以施主你该走了)

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    // 尝试循环获取任务
    for (;;) {
        
        // 省略部分代码...

        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        // 此时条件成立
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            // 执行成功
            if (compareAndDecrementWorkerCount(c))
                // 返回空任务,跳出当前循环。而外层runWorker()由于 task==null,也会跳出循环
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
} 

小结

如果把FutureTask也算上,实际上有4种执行多线程任务的方式:
● Thread:
○ 重写Thread的run()
○ 通过Thread的构造器传入Runnable实例
○ 通过Thread的构造器传入Runnable实例(FutureTask,内部包装了Runnable/Callable)
● 线程池:
○ 通过线程池(Runnable/Callable都行)

Thread和线程池看起来是单个线程和线程群体的关系,但实际上Thread和线程池还有个连接点:FutureTask。无论是Thread还是线程池,都可以接收FutureTask,只不过Thread使用FutureTask时需要我们在外部自己包装好Runnable和Callable,而线程池把这个操作内化了。

shutdown和shutdownNow

当我们频繁使用线程的时候,为了节约资源快速相应需求,我们会考虑用线程池,而线程池使用之后,就需要关闭,关闭一般会使用shutdown和shutdownNow:

区别

从字面意思就能理解,shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大;
shutdown()只是关闭了提交通道,用submit()是无效的;而内部该怎么跑还是怎么跑,跑完再停。

shutdown

将线程池状态置为SHUTDOWN,并不会立即停止:
停止接收外部submit的任务
内部正在跑的任务和队列里等待的任务,会执行完
等到第二步完成后,才真正停止

shutdownNow

将线程池状态置为STOP。企图立即停止,事实上不一定:
跟shutdown()一样,先停止接收外部提交的任务
忽略队列里等待的任务
尝试将正在跑的任务interrupt中断
返回未执行的任务列表

终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它也可能必须要等待所有正在执行的任务都执行完成了才能退出。但是大多数时候是能立即退出的。

总结

优雅的关闭,用shutdown()
想立马关闭,并得到未执行任务列表,用shutdownNow()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JDK线程池和Spring线程池都是用于管理和执行多线程任务的工具。它们有一些相似之处,但也有一些区别。 JDK线程池Java标准库中提供的一个线程池实现,位于`java.util.concurrent`包下。它提供了ThreadPoolExecutor类来创建和管理线程池。JDK线程池的主要特点包括: 1. 可以自定义线程池的大小和线程池的工作队列。 2. 提供了各种任务调度策略,例如固定大小线程池、缓存线程池、单线程池等。 3. 支持提交Callable和Runnable类型的任务,并返回Future对象以获取任务的执行结果。 4. 提供了一些监控和管理线程池的方法,例如获取线程池状态、关闭线程池等。 Spring线程池是在Spring框架中提供的一个对JDK线程池的封装。它基于JDK线程池,并提供了更高级别的功能和更方便的配置选项。Spring线程池的特点包括: 1. 可以通过Spring配置文件或注解来配置和管理线程池。 2. 支持异步方法调用,可以将某个方法调用标记为异步执行,从而将其放入线程池中执行。 3. 可以通过配置线程池的属性来控制并发执行的线程数量、线程池的队列大小、任务拒绝策略等。 4. 提供了对任务执行状态的监听和处理机制。 总的来说,JDK线程池Java标准库提供的一种多线程任务管理工具,而Spring线程池是在JDK线程池基础上提供的更高级别的封装,方便在Spring应用中使用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值