Java并发编程知识点总结(十八)——线程池浅析

1. 什么是线程池?

线程池和数据库的连接池的原理差不多,当需要线程工作的时候,就从线程池中获取一个空闲的线程来执行工作。当工作完成后,将线程池返回到线程池中,供其他任务使用。

2. 为什么要使用线程池?

使用线程池的优点主要有以下几个:

  1. 线程虽然是一个很轻量级的工具,但是创建和关闭依然需要花费一定的时间。如果每一个小任务都创建一个线程,那么很有可能创建和销毁线程的时间会大于实际工作的时间,这样得不偿失。
  2. 线程池可以起到管理的作用,如果无限制地创建线程,会十分消耗消耗系统的资源。因此线程池可以对线程进行管理。

3. 线程池的种类

JDK为了更好地管理线程,提供了一套Executor框架。下面是他们关系的UML图:

常见的线程池有5种,他们都是继承自ThreadPoolExecutor,所以ThreadPoolExecutor是Executor框架的核心。五种常见的线程池区别:

线程池作用
new FixedThreadPool(int nThreads)该方法返回一个固定数量的线程池,线程池中的线程的数量始终不变。
new SingleThreadExecutor()该方法返回一个只有一个线程的线程池。若有多个任务被提交到线程池,那么会保存在等待队列中。
new CachedThreadPool()该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定。如果有空闲线程,那么会优先复用。如果没有线程空闲,又有任务提交,则会创建新的线程
new SingleThreadScheduledExecutor()该方法返回一个ScheduledExecutorService对象,线程池大小为1.具有延时执行的功能。
ScheduledThreadPoolExecutor(int corePoolSize)该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量。

4. 线程池的创建

因为ThreadPoolExecutor是核心,所以创建线程池的工作也在这里完成。

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

上面的函数中有很多参数,我们来逐一进行解释:

  • corePoolSize:核心线程池中线程的数量,当线程池中线程的数量少于corePoolSize时,当有任务请求获得线程时,即使有空闲的线程,线程池也会继续创建线程,直到线程池中线程的个数大于等于corePoolSize为止。
  • maximumPoolSize:线程池中最大的线程数量,也就是线程池中线程总数不能超过maximumPoolSize。
  • keepAliveTime:空闲线程的存活时间,当线程池中的线程总数超过了corePoolSize时,如果有线程的空闲时间超过了keepAliveTime,那么就会对空闲的线程进行销毁。
  • unit:时间单位,也就是空闲线程存活时间的单位。
  • workQueue:阻塞队列,当线程池中的线程数量达到corePoolSize时,就会尝试将当前任务加入到阻塞队列中进行等待。例如可以使用ArrayBlockingQueue、LinkedBlockedQueue等
  • threadFactory:线程工厂,可以用来自定义线程的名字。
  • handler:拒绝策略,这个是当线程池中线程的数量达到maximumPoolSize,也就是达到饱和了,会执行拒绝策略。

5. 线程池的执行

我们接下来分析下线程池执行任务的流程:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        // 如果线程池中线程的数量小于corePoolSize
        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);
            // 如果线程池还活着,且线程数量为0
            else if (workerCountOf(recheck) == 0)
                // 创建一个没有具体命令的线程去执行等待队列中的任务
                addWorker(null, false);
        }
        // 加入阻塞队列失败,就尝试新建线程,如果超过maximumPoolSiz就执行拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }

通过上面的源码分析,我们可以看到线程池执行任务的流程包括3种可能

  1. 当线程池中线程的数量小于corePoolSize时,直接尝试创建线程去执行当前任务。
  2. 当线程池中线程的数量大于等于corePoolSize时,尝试将任务加入到阻塞队列中。
  3. 如果进入阻塞队列失败,那么就会尝试创建新的线程执行当前任务。如果线程池中线程的数量等于maximumPoolSize,就会导致创建线程失败,只能执行拒绝策略。

流程如下图所示:

6. 线程池的关闭

线程池的关闭主要涉及两个方法:shutdown、shutdownNow

方法作用
shutdown执行shutDown方法后,线程池不再接受新任务,等待任务执行完后关闭线程池
shutdownNow执行shutDownNow方法后,线程池不会等待任务执行完,直接关闭线程池

我们看下shutdown方法的源码:

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);  // 设置状态为shutDown
            interruptIdleWorkers();  // 销毁空闲的线程
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

再看下shutdownNow方法的源码:

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);  // 设置状态为STOP,直接停止
            interruptWorkers();  // 销毁所有线程
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

通过两段代码的对比,我们可以看到shutdownNow是不会等待线程执行完任务的,而shutdown是会等待任务执行完成。

7. 线程池使用示例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + "执行完任务:" + System.currentTimeMillis());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            System.out.println(thread.getName() + "提交任务到线程池:" + System.currentTimeMillis());
            executorService.submit(thread);
        }
    }
}

执行结果

Thread-0提交任务到线程池:1670595179593
Thread-1提交任务到线程池:1670595179594
Thread-2提交任务到线程池:1670595179594
Thread-3提交任务到线程池:1670595179594
Thread-4提交任务到线程池:1670595179594
Thread-5提交任务到线程池:1670595179594
Thread-6提交任务到线程池:1670595179594
Thread-7提交任务到线程池:1670595179594
Thread-8提交任务到线程池:1670595179594
Thread-9提交任务到线程池:1670595179594
pool-1-thread-2执行完任务:1670595180604
pool-1-thread-1执行完任务:1670595180604
pool-1-thread-3执行完任务:1670595180604
pool-1-thread-5执行完任务:1670595180604
pool-1-thread-4执行完任务:1670595180604
pool-1-thread-1执行完任务:1670595181608
pool-1-thread-4执行完任务:1670595181608
pool-1-thread-2执行完任务:1670595181608
pool-1-thread-3执行完任务:1670595181608
pool-1-thread-5执行完任务:1670595181608

在上面的代码中,我们创建了一个固定线程数为5的线程池,同时模拟了10个任务。从执行结果可以看到,每次只有5个线程在执行任务。

8. 线程池参数配置建议

  • 对于CPU密集型任务:配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。
  • 对于IO密集型任务:因为IO密集型任务大部分时间都在等待IO,对CPU消耗不大,因此建议配置尽可能多的线程数量,如配置2xNcpu。

参考文章:线程池ThreadPoolExecutor实现原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值