ThreadPoolExecutor详解

news2024/12/25 1:10:52

恰逢经济下行,鄙人工作、生活日趋艰难,原本美好的愿望,如今只能成为奢望。不知如何是好的我,只能精研近几年来因浮躁而荒废的知识。今天就想跟大家聊一个对我来讲看似熟悉实则陌生的工具——ThreadPoolExecutor。熟悉是因为在我负责的项目中它是一个出镜率最高演员;陌生是因为我对其内部工作原理一窍不通,而且它还是个高频考点。因此在这篇文章中,我想对其做个全面梳理,以便加深对它的理解,同时也希望通过这次梳理将那些累积已久的疑问全部解决掉。本次梳理的目标主要有以下几个:

  1. 弄清楚ThreadPoolExecutor的原理
  2. 彻底梳理清楚与ThreadPoolExecutor相关的知识

1 ThreadPoolExecutor简介

ThreadPoolExecutorJava平台java.util.concurrent包中的一个核心组件,它是一种线程池实现类,用于管理和调度线程以高效地执行一组可并行或异步处理的任务。其继承结构图如下所示:

由图可知ThreadPoolExecutor是ExecutorService的实现类(注意:ExecutorService接口继承了顶级接口Executor,因此ThreadPoolExecutor是顶级接口Executor的实现类)。由此可以知道ThreadPoolExecutor 提供了一套灵活且强大的线程池框架,允许用户自定义线程池的各种行为和参数,以适应不同应用场景的需求。另外,使用线程池也有很多好处,比如:可以减少创建和销毁线程的开销;提高系统的响应性和吞吐量。下面就让一起看一下线程池的主要特点:

  • 重用线程:线程可以在完成任务后被重用,而非每次任务都创建新的线程。(一直以来我都不清楚这个特征是怎么实现的,希望这篇文章能让我搞清楚这个问题)
  • 控制并发级别:可以通过设置线程池的最大大小来控制并发任务的数量。
  • 管理空闲线程:线程池可以管理空闲线程的存活时间,当没有任务执行时,线程会等待一定时间后终止。(关于这一点我也没有弄清楚是怎么回事)

下面再来看一下线程池ThreadPoolExecutor的核心参数,它的核心参数主要有下面几个,它们分别为(它们均定义在线程池的构造函数中):

  1. corePoolSize:线程池的基本大小,在任何时间都会维持这么多线程
  2. maximumPoolSize:线程池最大可以扩展到的线程数
  3. keepAliveTime:线程空闲时等待新任务的最长时间
  4. workQueue:用来存放等待执行任务的队列
  5. threadFactory:用于创建新线程的工厂
  6. handler:拒绝策略,当任务太多无法处理时采取的措施

下面就让我们一起看看线程池该如何使用。下面这个例子创建了一个线程池,其核心线程数为5,最大线程数为10,空闲线程的存活时间为60秒。如果提交的任务超过10个线程所能处理的范围,那么额外的任务将被存放在一个容量为5的阻塞队列中。如果队列也满了,那么再有任务提交时,将会根据指定的拒绝策略处理。在这个例子中,采用的是AbortPolicy,即抛出RejectedExecutionException异常:

import java.util.concurrent.*;

public class ThreadPoolExecutorExample {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            5, // corePoolSize
            10, // maximumPoolSize
            60L, // keepAliveTime
            TimeUnit.SECONDS, // timeUnit
            new ArrayBlockingQueue<>(5), // workQueue
            Executors.defaultThreadFactory(), // threadFactory
            new ThreadPoolExecutor.AbortPolicy() // handler
        );

        // 提交任务
        for (int i = 0; i < 15; i++) {
            final int taskId = i;
            Runnable task = () -> System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());
            executor.execute(task);
        }

        // 关闭线程池
        executor.shutdown();
    }
}

2拒绝策略

在前一小节中,我对ThreadPoolExecutor进行了简单的梳理,其中也指出了使用这个类时需要注意的一些核心数据,比如:核心线程数、最大线程数、线程等待时间、线程创建工厂、任务队列以及拒绝策略等。本小节我将梳理最后一个核心数据:拒绝策略。我一直在想这样一个问题:程序作为没有生命意识的实体,它真的会像人那样拒绝吗?被它拒绝的任务最后又去了哪里呢?“什么,居然有人问这种问题,他不会是个傻子吧!拒绝就是拒绝,拒绝就是不执行,拒绝就是不浪费计算机资源,这有那么难理解吗?”是的,现在的我肯定不会再问这种问题,但当初它确实让我苦恼了很久。站在线程池的角度,用生活中常见的例子再来理解一遍,被那个问题困扰很久的我仿佛痴子一般:作为一款只能处理十字纹的十字螺丝刀,你用它处理一字螺丝可行吗?作为需求方的你会怎么做?换螺丝刀。对!线程池就如这十字螺丝刀一样,它只知道自己要处理什么,该处理什么,没有义务为这个自己无法处理的任务提供解决方案,这是你需求方的事。是不是有点不近人情?但事实就是这样!螺丝刀不会讲话,只能你这个使用工具的人想其他办法。因此换到线程池中也是一样的,线程池只负责调度能力范围内的任务,对于那些自己无法处理的任务通知线程池使用方即可,由其按照自己的业务需求指定可靠的处理方案。因此现在看来,当时困扰我的问题其实是自己没有弄清主次造成的。下面就让我们一起看一下Java提供的几种内置的拒绝策略,每种策略都有其适用场景:

  • AbortPolicy:默认策略,简单地抛出 RejectedExecutionException 异常。这会导致调用者意识到任务被拒绝,并可能采取相应的补救措施
  • CallerRunsPolicy:调用者的线程直接运行任务。这意味着调用 execute 方法的线程会执行任务,而不是将其放入队列中等待执行。这种策略不会丢弃任务,但是可能会降低程序的整体性能
  • DiscardOldestPolicy:默默地丢弃任务,不抛出任何异常。这种策略适用于对任务丢失不敏感的情况
  • DiscardPolicy:丢弃队列中最旧的任务,然后重试执行当前任务。这种策略有助于确保较新的任务能够得到执行

以上4拒绝策略都实现了RejectedExecutionHandler接口,这个接口定义了线程池如何处理那些无法被接收的任务。具体可以参见下面这幅类结构图:

当线程池中的所有工作线程都在忙于处理任务,并且任务队列也已满时,如果此时仍有新的任务提交给线程池,就需要通过拒绝策略来决定如何处理这些任务。除了上述四种内置策略外,还可以自定义拒绝策略。譬如,可以通过记录日志、发送通知等方式来处理被拒绝的任务。下面看一个自定义拒绝策略示例,该策略记录被拒绝的任务信息,并尝试重新提交任务:

class CustomRejectHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (!executor.isShutdown()) {
            // 尝试重新提交任务
            try {
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                // 处理中断异常
                Thread.currentThread().interrupt();
            }
        } else {
            // 如果线程池已经关闭,则记录日志,或者将被拒绝的任务持久化到数据库中
            System.err.println("Task " + r.toString() + " rejected");
        }
    }
}

3 线程创建工厂

刚接触到这个属性时,我是非常困惑的:为什么这里会有这样一个属性调用线程池对象时传递的Runnable实现类不就是一个线程吗?直接执行这个线程不就把要执行的任务给执行了吗?从理论上讲,任务执行完后这个线程不是会被系统回收吗?那后续提交的任务怎么可能会有线程复用的情况发生呢?如果是这个思路,那这里为什么会有线程工厂这个属性呢?下面就让我们一步步的分析一下:

首先在Java中,ThreadFactory是一个接口,其主要作用是创建新的线程。ThreadFactory允许用户自定义线程创建的过程,这对于控制线程的属性(如优先级、线程组、名称等)等是非常有用的。java.util.concurrent包提供了ThreadFactory接口(注意:该接口中只有一个newThread(Runnable)方法),以及一些实现类。具体看下面可以参见下面这幅类结构图:

上图中的类均来自java.util.concurrent包,其中DefaultThreadFactory和PrivilegedThreadFactory两个类均为Executors类中的私有静态内部类。而DaemonThreadFactory类位于CompletableFuture类中。下面让我们回过头看一下开头提出的那个问题:为什么会有线程工厂这个属性?其实这个问题的答案在本小节第二段中:为了创建线程,所以这里要有一个ThreadFactory属性。换句话说,这个属性的主要作用是:通过可控的方式创建一个符合对应场景的线程对象。下面就来看一下DefaultThreadFactory类的定义:

private static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

从代码可以看出DefaultThreadFactory类通过new方式创建一个线程对象,并将其返回给调用者。这个类的调用入口在Worker类的构造方法中,调用该方法时会将当前Worker对象传递进去,具体代码为:getThreadFactory().newThread(this)

4 ThreadPoolExecutor究竟怎么实现的?

前面介绍了很多线程池使用上的知识,譬如相关的属性,特殊属性的自定义思路等。不过对于这个保存线程的容器,其究竟是怎么实现的,我个人并不了解。这节想就这个问题深入研究一番。首先作为存储线程的容器,其先要保证自身稳定,而后才能存放线程,这就像破了洞的水盆无法盛水一样;接着线程池要提供简单易用的API接口,以便于使用者快速使用。那java是如何实现这个盛放线程的容器的?主要分为以下几个方面:

  1. 容器。ThreadPoolExecutor本身就是一个容器,对于这个容器,其有自己的状态等属性。其中线程池的状态通过int类型的高3位来表示(理论上线程池共有7中状态),容器中线程的数量通过int类型的低29位来表示(理论上线程池可以存放268435455个线程,不知道这么理解是否正确)。因此ThreadPoolExecutor类提供了这样几个方法:runStateOf(int)、workerCountOf(int)、ctlOf(int, int)、runStateLessThan(int, int)、runStateAtLeast(int, int)、isRunning(int)、compareAndIncrementWorkerCount(int) 、decrementWorkerCount()、compareAndDecrementWorkerCount(int)等。这些方法都与判断线程池状态及数量等的操作有关。说到这里不得不提一下线程池的状态,java定义的线程池状态有:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。不知道这段理解有没有问题?
  2. 线程管理。为了更好的管理池中的线程,ThreadPoolExecutor定义了一些相关的属性,譬如:BlockingQueue<Runnable>类型的工作队列-workerQueue(主要用于存放使用方提交的任务)、int类型的最大线程数-maximumPoolSize、int类型的核心线程数-corePoolSize、long类型的线程超时时间-keepAliveTime、ThreadFactory类型的线程工厂-threadFactory、RejectedExecutionHandler类型的拒绝策略、HashSet<Worker>类型的工作线程。其他还有ReentrantLock类型的锁mainLock、long类型的largestPoolSize、long类型的completedTaskCount。(注意:ThreadPoolExecutor默认的拒绝策略是AbortPolicy)

一般使用线程池的方法非常简单,就是用线程池对象调用execute(new Runnable(){//实现run()方法})方法或submit(new Runnable(){//实现run()方法})。下面我想就execute()方法的执行流程谈谈自己的理解:

public void execute(Runnable command) {
// 检查传递进来的任务类是否为 null,如果为 null,则抛出空指针异常
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
// 获取正在运行的线程数,如果正在运行的线程少于核心线程数,则启动一个新线程处理这个任务
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}
//
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (int c = ctl.get();;) {
        // Check if queue empty only if necessary.
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            return false;

        for (;;) {
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateAtLeast(c, SHUTDOWN))
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int c = ctl.get();

                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

这段代码看起来很长,但这里要梳理的核心逻辑不是特别多。这段代码的具体的执行过程如下所示:

  1. 调用execute(Runnable command)方法,提交一个任务
  2. execute()方法会首先判断当前任务是否为null。a) 如果是,则直接抛出异常;b) 如果不是则拿到ThreadPoolExecutor中的计数器ctl的值,调用workerCountOf(ctl)值获取当前正在运行的工作线程的数量,比较这个值与核心线程数之间的关系,如果小于核心线程数,则调用addWorker()方法创建Worker对象,将其添加到ThreadPoolExecutor类的workers属性中,然后启动线程执行第一步提交的command。c) 如果正在运行的线程数大于核心线程数,则将提交的command添加到ThreadPoolExecutor类的workQueue队列中,如果添加成功则会继续检查看是否需要启动线程执行或者拒绝这个命令。d) 如果正在运行的线程数大于核心线程数,并且无法像任务队列中添加任务,那么直接调用addWorker()方法来创建Worker对象,并将其添加到ThreadPoolExecutor类的workers属性中,然后启动线程执行第一步提交的command。e) 如果这次调用addWorker()方法得到了false,则表示当前任务需要按照拒绝策略进行拒绝。(注意:d、e这两步的主要作用是判断当前运行的线程数与最大线程数之间的关系,如果小于就创建线程,如果大于就执行拒绝策略

5 Worker是什么?

Worker是ThreadPoolExecutor类中的一个私有内部类。它被final字段修饰,说明这个类不能被其他类继承。它实现Runnable接口,同时又继承AbstractQueuedSynchronizer类,因此Worker是一个线程类,可以被独立调用。在我的印象中所谓的线程就是一个具有独立业务处理逻辑的类,比如下面这个类:

class Task implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000 * 60 * 60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("aaaa");
        }
    }
}

从代码不难看出,这个类非常简单:它被调度后会休眠1小时,然后在控制台打印一串字符串aaaa。这有什么意义呢?我虽然无法明确其实际意义,但也不能简单地说它毫无意义。就像我们不能武断的认为一个不聪明的人毫无用处一样。既然如此,那就是说在工业级软件中存在这样的类?这种说法我觉得有点草率。还是一起来看看Worker吧!个人理解这个类就是一个工人,不过这个工人并非通常意义上的工人。这个工人大脑比较简单,只能识别一些简单的指令,比如吃饭、工作、休息等。为了更好的被别人操控,其甚至不顾家人反对再次自我阉割,完全成为一个毫无意识得工具人(别人说做什么,它就无条件做什么。说白了Worker就是一个打工人,是一个执行Runnable接口实现类的工具)。Worker类中有这样几个属性:

  1. long类型的serialVersionUID属性。这个属性是所有实现java.io.Serializable接口的类中必须存在的一个属性。
  2. Thread类型的thread属性。这个属性是用于调度Task线程的。这个Thread对象一般通过创建ThreadPoolExecutor类时指定的ThreadFactory对象创建。
  3. Runnable的firstTask属性。这个属性的值一般是调度ThreadPoolExecutor对象时指定的Runnable接口实现类,比如上面那个具有打印功能的Task对象。
  4. valatile long类型的completedTasks属性。这个属性用于记录当前Worker线程执行的任务的数量。

Worker类提供的方法有这样几个:

  1. public void run():这个方法来自于Runnable接口。其核心代码为runWorker(this),这是ThreadPoolExecutor类中的一个方法。该方法执行逻辑为:1)取出当前的线程对象(Thread wt=Thread.currentThread(););2)从Worker对象中取出任务线程对象(Runnable task=w.firstTask),同时将Worker对象的firstTask属性设置为null(w.firstTask = null;);3)调用Worker对象上的unlock()方法(w.unlock());4)通过while循环的方式判断取出的task对象是否null,如果不为null,则进入while循环体,否则执行completedAbruptly=false,并执行finally中的代码(调用processWorkerExit(Worker对象, completedAbruptly)方法)【关于while循环体的执行逻辑的梳理:调用Worker对象上的lock()方法;判断线程池的状态,如果线程池被终止,则调用当前线程对象wt上的interrupt()方法;否则执行beforeExecute(wt, task)方法,执行task上的run()方法,接着执行afterExecute(task, null)方法。如果上面执行过程抛出异常,则执行catch块的afterExecute(task, null),否则执行finally块中的代码(task=null;w.completedTasks++;w.unlock();)】。这里有一点需要注意:通过runWorker()方法的执行逻辑可以看出,提交给线程池的任务虽然实现了Runnable接口,但其并没有被线程池当作一个线程对象使用,也就是说并未将该对象交由Thread对象管理并启动新线程处理,而是直接调用其中的run()方法。直白讲线程池将实现Runnable接口的任务类当作一个普通的java对象使用;beforeExecute(Thread t, Runnable r)和afterExecute(Runnable r, Throwable t)两个方法被定义在ThreadPoolExecutor类中。
  2. protected boolean isHeldExclusively():该方法源于AbstractQueuedSynchronizer接口,表示当前锁是否处于占用状态
  3. protected boolean tryAcquire(int unused) :这个方法来自于AbstractQueuedSynchronizer接口,表示尝试加锁(当状态为0的时候获取锁,CAS操作成功,则state状态为1)
  4. protected boolean tryRelease(int unused) :这个方法来自于AbstractQueuedSynchronizer接口,用于释放锁(释放锁,将同步状态置为0)
  5. public void lock():这个是Worker对外提供的加锁方法。
  6. public boolean tryLock():这个是Worker对外提供的尝试加锁方法
  7. public void unlock():这个是Worker对外提供的解加锁方法
  8. public boolean isLocked():这个是Worker对外提供的查看当前锁是否锁定的方法
  9. void interruptIfStarted():这个是Worker对外提供的一个线程中断方法。

6 线程池的调度细节

在第5、第6小节中我们梳理了线程池任务提交的流程以及Worker类的作用,这一小节我将在这两小节的基础上梳理一下线程池调度任务的具体过程。首先在这里啰嗦几句:前面梳理任务提交流程时,我们看到在addWorker(Runnable, true)中有这样一段逻辑,具体见下图:

通过这段代码可以看出我们交给线程池的任务,最终由Worker对象托管(这个从new Worker(Runnable)这段代码即可看出),这个类在第6小节中梳理过,其有一个Runnable类型的属性firstTask,由一个Thread类型的属性thread,其中firstTask表示的是我们提交给线程池的任务,thread表示的是调度线程的Thread对象。在创建完Worker对象后,会从Worker对象中取出其持有的Thread对象,然后执行t.start()方法。注意:调用start()方法后,Worker所代表的工作线程就开始工作了(为什么会这样?)。下面让我们看一下Worker类的的构造方法和run()方法的源码:

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

public void run() {
    runWorker(this);
}
// runWorker() 方法位于 ThreadPoolExecutor 类中
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
// ***************************************
    Runnable task = w.firstTask;
// ***************************************
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                try {
// ***************************************
                    task.run();
// ***************************************
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

从这段代码中的构造方法中不难发现,Worker对象持有的Thread对象最终持有的Runnable对象就是当前的Worker对象,所以当执行前面说的t.start()时,实际调度的就是当前的Worker线程。那这个Worker线程究竟做了什么?这还得看Worker线程的run()方法,从这个方法的源码不难知道,其调用了ThreadPoolExecutor类中的runWorker(Runnable)方法。这个方法的做的事情也很简单:从Worker对象中拿到Runnable类型的task,然后调用该任务上的run()方法(具体参见上述源码中星号标识的代码)。通过梳理,我们可以发现提交给线程池的任务,虽然实现了Runnable接口,但最终只是被线程池当作一个普通的java对象使用,而非线程对象

7 总结

通过这篇文章,我对ThreadPoolExecutor的认识变得更加清晰,更加全面和系统化。这与之前那种觉得懂却又说不出个所以然的状态相比,显然进步了不少。这也是我在平台发布这篇文章的目的。通过这篇文章我主要学习到了以下几点:

  1. 线程池的基本知识。ThreadPoolExecutorJava平台java.util.concurrent包中的一个核心组件,它是一种线程池实现类,用于管理和调度线程以高效地执行一组可并行或异步处理的任务
  2. 线程池的核心参数有:核心线程数、最大线程数、线程工厂、拒绝策略、线程超时时间、任务队列等
  3. 线程池的任务提交流程为:先判断当前线程池中是否存在正在运行的工作线程,并判断其与核心线程数的关系。如果小于核心线程数,就创建工作线程并执行当前任务;如果大于核心线程数,则将当前提交的任务放置到任务队列中等待有空闲线程时进行调度;如果任务队列中不存在足够的空间,则判断线程池中活跃的线程数与最大线程数的关系,如果小于最大线程数,则创建新的工作线程执行当前被提交的任务;如果大于最大线程数,则启动拒绝策略。java线程池主要提供了四种拒绝策略:AbortPolicy(默认,直接抛出RejectedExecutionException);CallerRunsPolicy(调用者的线程直接运行任务。这意味着调用 execute 方法的线程会执行任务,而不是将其放入队列中等待执行。这种策略不会丢弃任务,但是可能会降低程序的整体性能);DiscardOldestPolicy(默默地丢弃任务,不抛出任何异常。这种策略适用于对任务丢失不敏感的情况);DiscardPolicy(丢弃队列中最旧的任务,然后重试执行当前任务。这种策略有助于确保较新的任务能够得到执行)
  4. Worker是ThreadPoolExecutor类中的一个私有内部类。其本质就是一个打工仔,是一个执行实现Runnable接口的任务的工具
  5. 被线程池调度的任务,虽然实现了Runnable接口,但其仅仅被作为普通java对象用,而非线程对象使用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2051245.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【nacos 第二篇章】动手实践(从零代码开发版)

一、环境准备 本章将通过手把手的教程一步一步教你如何从零开发一个微服务应用。 首先需要安装好 nacos 服务并启动。安装 nacos 服务请看作者的 【nacos 第一篇章】安装一下 nacos 文章。 二、初始化项目 如上图所示&#xff0c;可以建立一个基础的项目。 搭建了基础项目之…

计算机毕业设计 在线项目管理与任务分配系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

节点使用简介:comfyui-photoshop

1、安装comfyui-photoshop 略过 一点要注意的是&#xff1a;在Photoshop上的安装增效工具&#xff0c;要通过Creative Cloud 桌面应用程序进行安装&#xff0c;才能成功在增效工具中显示&#xff0c;直接通过将文件解压到Plug-ins路径行不通&#xff08;至少对我来说行不通&am…

通过剪枝与知识蒸馏优化大型语言模型:NVIDIA在Llama 3.1模型上的实践与创新

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

阿里云ACP的三种报名与对应题库获取方式的详细说明(按费用排序)

文章目录 前言方式一、官方途径(较为昂贵)考试资格获取官方视频教程获取方式总结 方式二、报名机构(价格适中&#xff0c;考取速度快)推荐机构大概费用机构报名方式机构报名后所携带的内容或者说对于其他方法有什么优势总结 方式三、闲鱼(最便宜&#xff0c;但题库有风险)考试资…

C语言 之 strstr函数的使用和模拟、strtok函数的使用、strerror函数和perror函数的使用

文章目录 strstr 的使用和模拟实现strstr函数模拟实现 strtok 函数的使用例子1例子2例子3 strerror 函数的使用perror函数 strstr 的使用和模拟实现 函数原型&#xff1a; const char * strstr ( const char * str1, const char * str2 ); 该函数能够查找str1中第一次出现str2…

产业经济大脑建设方案(五)

为了提升产业经济的智能化水平&#xff0c;我们提出建设一个综合产业经济大脑系统&#xff0c;该系统通过整合大数据分析、人工智能和云计算技术&#xff0c;构建全方位的数据采集、处理和决策支持平台。该平台能够实时监测产业链各环节的数据&#xff0c;运用智能算法进行深度…

Unified 阻抗控制 architecture、framework、approach

Unified 阻抗控制&#xff08;Unified Impedance Control&#xff09;作为一种控制策略&#xff0c;其architecture&#xff08;架构&#xff09;、framework&#xff08;框架&#xff09;和approach&#xff08;方法&#xff09;为&#xff1a; 一、Unified 阻抗控制 Archite…

【MPC】模型预测控制 | 车辆优化控制策略研究与实践

写在前面&#xff1a; &#x1f31f; 欢迎光临 清流君 的博客小天地&#xff0c;这里是我分享技术与心得的温馨角落。&#x1f4dd; 个人主页&#xff1a;清流君_CSDN博客&#xff0c;期待与您一同探索 移动机器人 领域的无限可能。 &#x1f50d; 本文系 清流君 原创之作&…

CorelDRAW X4重磅发布更新!包含最新安装包下载

CorelDRAW X4是矢量图形制作工具软件&#xff0c;这个图形工具给设计师们提供了矢量动画、页面设计、网站制作、位图编辑和网页动画等多种功能。 X4版本的新特性有活动文本格式&#xff1a;仿效现有段落格式化、使文本能够贴齐格线框&#xff0c;或从边框缩排&#xff0c;从而获…

牛客JS题(四十四)根据包名,在指定空间中创建对象

注释很详细&#xff0c;直接上代码 涉及知识点&#xff1a; 地址引用assign 题干&#xff1a; 我的答案 <!DOCTYPE html> <html><head><meta charset"UTF-8" /><style>/* 填写样式 */</style></head><body><!--…

使用多种机器学习模型进行情感分析

使用 TF-IDF 与贝叶斯分类器进行情感分析是一个常见且有效的组合&#xff0c;特别是在文本分类任务中。贝叶斯分类器&#xff08;通常是朴素贝叶斯分类器&#xff09;等机器学习模型具有计算简单、效率高的优点&#xff0c;且在文本分类任务中表现良好。接下来&#xff0c;我将…

8.16作业

1.思维导图 2.在登录界面的登录取消按钮进行以下设置&#xff1a; 使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断…

Solidworks二次开发:通过XYZ点的曲线

在SolidWorks中,通过XYZ点创建曲线是一种根据一组点的坐标生成三维曲线的方法。这种方法适用于需要根据特定点集设计曲线的情况,比如在建模复杂几何形状或执行逆向工程时。在SolidWorks中通过XYZ点创建曲线,操作步骤如下 打开SolidWorks并新建文件:启动SolidWorks软件,新建…

开放式耳机哪个品牌好用?盘点四款开放式蓝牙耳机排行榜前十名

作为网易云十级选手&#xff0c;测评过三十多款开放式耳机产品的开放式耳机测评专家来说&#xff0c;如果想要入手一款好用的开放式耳机的话&#xff0c;我会建议从三个方面来考虑&#xff0c;佩戴体验、音质效果和性能配置&#xff0c;了解这三个方面选购款好用的开放式耳机不…

Linux配置JDK8环境变量

目录 一、yum安装1.1 OpenJDK安装1.2 测试是否能够使用1.3 如何卸载JDK 二、手动安装2.1 下载2.2 上传到linux服务器路径2.3 解压2.4 配置环境变量2.5 测试是否能够使用 一、yum安装 1.1 OpenJDK安装 sudo yum install -y java-1.8.0-openjdk-devel1.2 测试是否能够使用 jav…

vue3+高德地图实现,创建和编辑多边形、圆形覆盖物(完整实现)

目录 一、绘制 1.形状选择 2.绘制图形 二、编辑 1.定义所需变量 2.获取需要编辑的覆盖物数据 fenceData并渲染 3.上述监听改动事件↓↓↓ 4.上述监听编辑结束事件 5.鼠标右击结束事件 初始化地图不再赘述,参考这篇文章 vue3中引入高德地图初始化,并添加、删除marke…

MySQL(四)——常用函数

文章目录 函数字符串函数数值函数日期函数流程函数 函数 函数&#xff0c;是指一段可以直接被另一段程序调用的程序或代码。 MySQL中内置了许多函数&#xff0c;我们只需在合适的场景下调用它们即可&#xff0c;调用函数查询结果直接使用SELECT即可&#xff0c;并且可以嵌套使…

电子电气架构---主流主机厂电子电气架构华山论剑(上)

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 屏蔽力是信息过载时代一个人的特殊竞争力&#xff0c;任何消耗你的人和事&#xff0c;多看一眼都是你的不…

递归--数据结构--黑马

递归 总结一句话&#xff0c;上手直接多刷Leetcode&#xff0c;比看这个更有用。 定义 递归是一种解决计算问题的方法&#xff0c;其中解决方案取决于同一类问题的更小子集。 例如&#xff0c;单链表递归遍历的例子&#xff1a; void f(Node node) {if (node null) {retu…