一文搞懂JAVA中的线程池

写在前面

文章的前面部分是介绍四种线程池已经如何使用线程池,后面会对线程池的机制做一个详细的介绍,如果只是为了知道如何使用线程池,只需要看前面部分即可。

线程池的原理

其实线程池运用的是一种池化的思想,能自动地帮我们去管理线程,线程的创建和销毁都依赖于线程池去实现。如果我们每次都独立地去创建线程,我们很难对我们创建过的线程进行一个高效的管理,比如说当我们的线程执行完之后该如何销毁等等问题,而线程池就能够处理这个问题,它是一个容器,里面有一或多个线程在执行,有一个统一的策略去管理里面的线程。官方一点讲就是:线程池的核心思想是通过维护一组线程,合理地管理任务的执行,提高系统的性能和稳定性。通过适当调整线程池的参数,可以使得线程池适应不同规模和负载的任务需求。

四种线程池的介绍以及使用场景分析

newFixedThreadPool

特点:

  • 没有非核心线程数,核心线程数和最大线程数一致,由传入参数决定。
  • keepAliveTime(非核心线程存活时间):0s,也就是说非核心线程一旦没有任务就立即被销毁。不过newFixedThreadPool也没有非核心线程,所以线程空闲也不会被销毁。
  • 阻塞队列:LinkBlockingQueue

可以创建容纳一定数量的线程池,池子里的所有线程都是核心线程,每个线程存活的时间都是无限的,当线程数达到核心线程数,就不在添加新的线程,如果核心线程都在繁忙中,新添加的任务会加入阻塞队列。

使用场景:适用于执行长期任务,性能比较好这种线程池,适用于需要限制并发线程数量的场景,可以避免因为线程过多而导致系统负载过大的问题。

newCachedThreadPool

特点:

  • 核心线程数:0
  • 最大核心线程数:Integer.MAX_VALUE
  • keepAliveTime: 60s
  • 阻塞队列:SynchrnousQueue

因为线程池的核心线程数为0,所以当一个新的任务交给线程池去执行的时候会插入SyncchrnousQueue队列中,如果有空闲的线程就交给空闲的线程,没有则新建一个线程去执行任务。需要注意的是在一些极端的情况下,太多的任务可能会导致线程池新建太多的线程而导致耗尽cpu的资源。同样的,因为核心线程数为0,所以空闲的newCachedThreadPool不会占用任何cpu资源。

使用场景: 执行很多短期异步的小程序或者负载较轻的服务器

newSingleThreadPool

特点:

  • 核心线程数:1
  • 最大线程数:1
  • keepAliveTime: 0s
  • 阻塞队列:LinkBlockingQueue

创建一个只有一个线程的线程池,且线程的存活时间为无限。当线程池已经在执行任务的时候,有新任务加入则加入阻塞队列。

使用场景:适用于需要一个个执行的任务场景

newScheduledThreadPool

特点:

  • 核心线程数:由传入的参数决定
  • 最大核心线程数:Interger.MAX_VALUE
  • keepAliveTime: 0s,非核心线程一旦空闲就被销毁
  • 阻塞队列:延迟队列DelayedWorkQueue,按超时时间升序排序的队列。

创建一个固定大小 的线程池,线程池内非核心线程 一旦空闲就会被销毁,线程池可以支持定时及周期性任务 执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构,任务执行完后将time修改位下次被执行时间,然后将任务放回延迟队列中。

使用场景:周期性执行任务的场景

线程池的使用

创建线程池: 使用Executors类提供的静态方法创建不同类型的线程池选择适合你需求的线程池类型和大小。

ini 复制代码
 public  static  ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

提交任务

execute()方法

ini 复制代码
cachedThreadPool.execute(() -> {
          ......
        });
    }

使用线程池的submit()方法提交需要执行的任务,可以是Runnable接口或Callable接口的实现类。

less 复制代码
javaCopy code
executor.submit(new MyRunnableTask());

关闭线程池: 在程序结束时,需要显式地关闭线程池,释放资源。

ini 复制代码
executor.shutdown();

submit() 方法与execute() 方法的区别:

在Java的Executor框架中,Executor接口提供了两种主要的任务提交方法:submit()execute()。它们之间的主要区别在于返回值和异常处理:

1. submit() 方法:

submit() 方法用于提交一个实现了Callable接口的任务或者一个Runnable接口的任务。它有以下特点:

  • 返回值: submit() 方法返回一个Future对象,你可以通过这个Future对象获取任务的执行结果,或者等待任务执行完成。

    ini 复制代码
    javaCopy codeExecutorService executor = Executors.newFixedThreadPool(10);
    Future<String> futureResult = executor.submit(new CallableTask());
  • 异常处理: 如果任务抛出了异常,可以通过调用Future对象的get()方法获取到异常,或者使用isDone()方法来判断任务是否完成。

    dart 复制代码
    javaCopy codetry {
        String result = futureResult.get();
        // 处理任务执行结果
    } catch (InterruptedException | ExecutionException e) {
        // 处理任务执行过程中的异常
    }

2. execute() 方法:

execute() 方法用于提交一个实现了Runnable接口的任务。它有以下特点:

  • 返回值: execute() 方法没有返回值,它是void类型的,不提供任何关于任务执行情况的返回信息。

    ini 复制代码
    javaCopy codeExecutorService executor = Executors.newFixedThreadPool(10);
    executor.execute(new RunnableTask());
  • 异常处理: 如果任务抛出了异常,无法通过execute()方法获取到异常。需要在Runnable的具体实现中进行异常处理,或者使用try-catch块来捕获异常,以免任务抛出异常后导致线程池中的线程终止。

    typescript 复制代码
    javaCopy codepublic class RunnableTask implements Runnable {
        @Override
        public void run() {
            try {
                // 任务逻辑
            } catch (Exception e) {
                // 处理任务执行过程中的异常
            }
        }
    }

综上所述,主要区别在于submit()方法提供了对任务执行结果的获取和异常处理的能力,而execute()方法则不提供这些能力,只能用于提交不需要获取结果的简单任务。选择方法应该根据任务的需求来决定是否需要获取返回值和处理异常。

线程池的构造参数

ThreadPoolExecutor构造函数参数:

  1. corePoolSize:核心线程数,即使在空闲状态,也会在线程池中存在的线程数量,除非设置了allowCoreThreadTimeOut
  2. maximumPoolSize:允许在线程池中的最大线程数量。当工作队列已满且池中线程数小于最大线程数时,线程池会创建新线程来处理任务,直到达到最大线程数为止。
  3. keepAliveTime:非核心线程最长空闲时间,超过这个时间,空闲的非核心线程会被回收,设置allowCoreThreadTimeOut=true,同样也会作用在核心线程中。
  4. unit:时间单位,用于指定keepAliveTime的时间单位,可以是TimeUnit.MILLISECONDSTimeUnit.SECONDS等。
  5. workQueue:存储将被execute方法执行的Runnable任务的阻塞队列。
  6. threadFactory :线程工厂,用于创建线程的工厂。可以自定义线程的名称、优先级等属性。
  7. RejectedExecutionHandler :拒绝策略,线程池的饱和策略事件。

ctl

ThreadPoolExecutor中维护了一个叫做ctl的AtomicInteger,ctl包括两部分,高三位代表线程池的状态,低29位代表线程池工作线程的数量

线程池共有五种状态:

  • 运行状态(RUNNING): 这个状态下的线程池,既接收新的任务,也处理已添加任务。
  • 关闭状态(SHUTDOWN): 这个状态不再接受新的任务,但是会处理已经添加到工作队列中的任务,如果调用了shutdown()方法,线程池会切换到这个状态。
  • 停止状态(STOPING): 这个状态不再接收新的任务,也不处理工作队列中的任务,中断正在运行的任务。如果调用了shutdownNow()方法,线程池会切换到这个状态。
  • 整理状态(TIDYING): 这个状态下的工作线程数量为0,所有任务都已经终止,当线程池切换到这个状态后,会调用terminated()方法,这个方法可以被重写以在线程池彻底终止之前执行一些清理操作。
  • 终止状态(TERMINATED):线程池彻底终止,不再处理任何任务。

这些状态主要是通过ThreadPoolExecutor类的内部状态变量来表示和管理的。在线程池的生命周期中,它会依次经历这些状态。具体的状态切换由线程池的内部逻辑来管理,开发者一般无需手动干预状态的切换。通常,我们只需要关心线程池的任务提交和关闭操作,线程池会根据任务的提交和关闭来自动管理状态。

阻塞队列

  • ArrayBlockingQueue : 有界队列,基于数组实现。接受一个int参数来指定队列的大小,通过FIFO(First-In-First-Out:先入先出)方式读取队列中的任务。
  • LinkedBlockingQueue : 基于链表实现的阻塞队列,容量可以设置,如果不设置则为无界队列。最大值为Integer.MAX_VALUE。
  • LinkedTransferQueue:基于链表实现的无界阻塞队列
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
  • DelayQueue:延迟队列,是一个任务定时周期的延迟执行的队列,根据指定的执行时间从小到大排序,否则根据插入队列的先后排序。
  • PriorityBlockingQueue:优先级队列,具有优先级的无界阻塞队列。存入PriorityBlockingQueue的元素需要实现Comparator接口,Comparator接口实现决定了元素存取顺序。
  • SynchronousQueue:同步队列,一个不存储元素的阻塞队列。生产者-消费者模式。在SynchronousQueue中,一个任务被插入之后,必须等待另一个线程调用移除操作,否则插入操作一直处于阻塞状态。吞吐量通常要高于LinkedBlockingQueuenewCachedThreadPool线程池使用了这个队列。

拒绝策略

以下是Java中内置的几种拒绝策略:

  1. AbortPolicy(默认策略): 当任务无法被执行时,会抛出RejectedExecutionException异常。

    java 复制代码
    javaCopy codeThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()
    );
  2. CallerRunsPolicy: 当任务无法被执行时,会由提交任务的线程执行该任务。这种策略可以降低向线程池提交任务的速度。

    java 复制代码
    javaCopy codeThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );
  3. DiscardPolicy: 当任务无法被执行时,会默默地丢弃该任务,不会抛出异常也不会执行任务。

    java 复制代码
    javaCopy codeThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.DiscardPolicy()
    );
  4. DiscardOldestPolicy: 当任务无法被执行时,会丢弃队列中等待时间最长的任务,然后将新任务加入队列。

    java 复制代码
    javaCopy codeThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.DiscardOldestPolicy()
    );
  5. 自定义拒绝策略: 你也可以实现RejectedExecutionHandler接口,定义自己的拒绝策略。例如:

    java 复制代码
    javaCopy codepublic class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            // 自定义的拒绝策略逻辑
            // ...
        }
    }
    ​
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(),
        Executors.defaultThreadFactory(),
        new CustomRejectedExecutionHandler()
    );

任务执行

线程池执行任务,调用execute()方法:

scss 复制代码
// ThreadPoolExecutor # execute()
public void execute(Runnable command) {
    // 任务为空,抛出空指针异常
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // 1. 判断当前正在运行线程数是否小于核心线程池,是则新创建一个线程执行任务,否则将任务放到任务队列中
    if (workerCountOf(c) < corePoolSize) {
        // 在addWorker中创建工作线程执行任务
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2. 判断线程池是否处于运行状态,且判断向任务队列中插入任务是否成功
    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);
}

线程池执行任务流程如下:

  1. 当前工作线程数未超过核心线程数时,就直接创建一个核心线程去执行任务
  2. 当线程池中核心线程数已满,就将任务添加到任务队列workQueue中排队等待执行
  3. 当线程池中核心线程数已满且任务队列workQueue也满时,判断工作线程数是否到达最大线程数,如果没到达,创建一个非核心线程去执行任务
  4. 当线程数量超过最大线程数时,直接采用拒绝策略处理,也就是通过RejectedExecutionHandler通知任务调用者

流程图如下:

添加工作线程

在线程池任务执行的时候,我们知道通过调用addWorker()方法可以向线程池中添加核心线程或非核心线程。

ini 复制代码
// ThreadPoolExecutor # addWorker()
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        // 1. 首先进行了一次线程池状态的检测:
        int c = ctl.get();
        int rs = runStateOf(c);
​
        // Check if queue empty only if necessary.
        //判断当前线程池的状态是不是已经shutdown,如果shutdown了拒绝线程加入
        //(rs!=SHUTDOWN || first!=null || workQueue.isEmpty())
        // 如果rs不为SHUTDOWN,此时状态是STOP、TIDYING或TERMINATED,所以此时要拒绝请求
        // 如果此时状态为SHUTDOWN,而传入一个不为null的任务,那么需要拒绝请求
        // 如果状态为SHUTDOWN,同时队列中已经没任务了,那么需要拒绝请求
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
​
        for (;;) {
            int wc = workerCountOf(c);
            // 2. 如果当前的数量超过了CAPACITY,或者超过了corePoolSize和maximumPoolSize(试core而定),那么需要拒绝请求
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 3. compareAndIncrementWorkerCount先将正在运行的线程数+1,数量自增成功则跳出循环,自增失败则继续从头继续循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        // 4. 将任务封装为一个Worker对象
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            // workers是HashSet线程不安全的,那么此时需要加锁
            final ReentrantLock mainLock = this.mainLock; //全局锁
            //获取全局锁
            mainLock.lock();
            try {
                // 当持有了全局锁的时候,还需要再次检查线程池的运行状态等
                int rs = runStateOf(ctl.get());
                // 线程池处于运行状态,或者线程池关闭且任务为空
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    // 线程处于活跃状态,即线程已经开始执行或者还未死亡,正确的线程在这里应该是还未开始执行的
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    //private final HashSet<Worker> workers = new HashSet<Worker>();
                    // 包含线程池中所有的工作线程,只有在获取了全局锁的时候才能访问它。将新构造的工作线程加入到工作线程集合中
                    workers.add(w);
                    // 工作线程数量
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    // 新构造的工作线程加入成功
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 在被构造为Worker工作线程,且被加入到工作线程集合中后,执行线程任务,注意这里的start实际上执行Worker中run方法
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        //未能成功创建执行工作线程
        if (! workerStarted)
            //在启动工作线程失败后,将工作线程从集合中移除
            addWorkerFailed(w);
    }
    return workerStarted;
}

主要实现如下:

  1. 检查线程池状态,如果线程池不是RUNNING状态,则不允许添加工作线程
  2. 判断线程数是否最大容量,核心线程数是否已满或最大线程数是否已满(根据传入的core参数而定),若其中一个条件满足则不允许添加工作线程
  3. 通过CAS操作将工作线程数+1,若失败则重试上述操作
  4. 将任务封装成一个Worker对象
  5. worker添加到workers集合中,由于workers是一个HashSet,因此需要加锁保证线程安全,然后检查线程池状态是否为RUNNING状态,或SHUTDOWN状态且任务为空,然后将worker添加到workers集合中,释放锁。如果worker添加成功,调用对应线程的start()方法。

Worker类是ThreadPoolExecutor的一个内部类,继承AQS,实现了Runnable接口,看看Worker的构造方法:

scala 复制代码
// ThreadPoolExecutor # Worker
private final class Worker extends AbstractQueuedSynchronizer implements Runnable
{
    private static final long serialVersionUID = 6138294804551838833L;
​
    // 保存执行任务的线程,从线程工厂获取,创建失败则为Null
    final Thread thread;
    // 要执行的初始任务,可能为Null
    Runnable firstTask;
    /** Per-thread task counter */
    volatile long completedTasks;
​
    // Worker是对firstTask的包装,并且Worker本身就是Runnable
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        //通过线程工厂来创建一个线程,将自身作为Runnable传递传递
        this.thread = getThreadFactory().newThread(this);
    }
​
    /** Delegates main run loop to outer runWorker. */
    public void run() {
        runWorker(this);
    }

首先保存了传入的任务,然后从线程工厂中创建一个线程。

获取任务

线程启动后,最终会调用到Worker#run()方法,然后调用runWorker()方法:

ini 复制代码
// ThreadPoolExecutor # runWorker()
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    //标识线程是不是异常终止的
    boolean completedAbruptly = true;
    try {
        //task不为null情况是初始化worker时,如果task为null,则去队列中取线程--->getTask()
        while (task != null || (task = getTask()) != null) {
            //获取worker的锁,防止线程被其他线程中断
            w.lock();
            // 如果线程池停止,确保线程被中断,否则,确保线程不被中断
            // 需要二次检查
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                // 线程开始执行之前执行此方法,可以实现Worker未执行退出,本类中未实现
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 执行任务
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    //任务执行后执行,可以实现标识Worker异常中断的功能,本类中未实现
                    afterExecute(task, thrown);
                }
            } finally {
                //运行过的task标null
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        //处理worker退出的逻辑
        processWorkerExit(w, completedAbruptly);
    }
}

这里首先检查了线程池的状态,如果线程池处于STOP状态,则必须保证线程处于中断状态,首先执行Worker中的首个任务firstTask,如果没有或者已经执行过了则调用getTask()方法从任务队列中获取任务,然后执行。

下面看看getTask()方法如何获取任务队列中的任务:

ini 复制代码
// ThreadPoolExecutor # getTask()
// 队列中获取线程
private Runnable getTask() {
    // 判断后面的poll是否要超时
    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);
​
        // Are workers subject to culling?
        // 标识着当前Worker超时是否要退出
        // wc > corePoolSize时需要减小空闲的Worker数,那么timed为true
        // 但是wc <= corePoolSize时,不能减小核心线程数timed为false
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
​
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        // 如果timed为true那么使用poll取任务。如果正常返回,那么返回取到的task
        // 如果超时,证明worker空闲,同时worker超过了corePoolSize,需要删除,返回r=null
        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
            workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
相关推荐
憨子周42 分钟前
2M的带宽怎么怎么设置tcp滑动窗口以及连接池
java·网络·网络协议·tcp/ip
霖雨2 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器
SRY122404192 小时前
javaSE面试题
java·开发语言·面试
Fiercezm3 小时前
JUC学习
java
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言
ZIM学编程3 小时前
Java基础Day-Sixteen
java·开发语言·windows
我不是星海3 小时前
1.集合体系补充(1)
java·数据结构
P.H. Infinity3 小时前
【RabbitMQ】07-业务幂等处理
java·rabbitmq·java-rabbitmq
爱吃土豆的程序员3 小时前
java XMLStreamConstants.CDATA 无法识别 <![CDATA[]]>
xml·java·cdata
2401_857610034 小时前
多维视角下的知识管理:Spring Boot应用
java·spring boot·后端