深入理解线程池(一)

1.线程池的重要性

什么是“池”?我们可以把池理解为计划经济,它是有一层线程的集合的含义。如果我们没有使用线程池,每个任务都需要去新开一个线程处理,那么如果在任务很多的时候呢?我们需要创建大量的线程来执行吗?对于开发人员来说,计算机的资源其实是有限而且宝贵的,线程是操作系统能够进行运算调度的最小单位,当我们创建大量的线程时,cpu对这些大量的线程无限调度并最终导致资源耗尽,因而可能因为部分的任务而对整体的性能造成巨大的影响。况且对于一些执行时间短的任务,频繁的创建和销毁线程是“性价比”极低的。

总结一下两个问题:

  1. 反复创建线程开销大
  2. 过多的线程会占用太多的内存

解决上述两个问题的思路:

  1. 用少量的线程——避免内存占用过多
  2. 让这部分线程保持工作,且可以反复执行任务——避免生命周期损耗

线程池也有以下的好处

  1. 加快响应速度
  2. 合理利用CPU和内存
  3. 统一管理资源

2. 线程池适合使用的场景

  • 服务器接受到大量的请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率
  • 开发中,如果需要创建五个以上的线程,那么就可以使用线程池来管理

3.线程池的使用

相信大部分小伙伴都使用过线程池,那么在使用的过程中,会不会思考几个问题呢

  1. 线程池构造函数的参数是什么?各自代表什么含义?
  2. 线程池应该手动创建还是自动创建?会有什么问题?
  3. 线程池里的线程数量设定多少比较合适?
  4. 如何正确的停止线程池呢?

下面我们就围绕这几个问题进行展开的实验与讲解

3.1线程池的构造

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;
    }

通过源码我们可以看到线程池的构造函数是有六个的,分别如下
在这里插入图片描述
参数详解

  1. corePoolSize:指的是核心线程数,线程池在完成初始化后,默认情况下,线程池中并没有任何线程(懒加载),线程池会等待有任务到来时,再去创建新线程去执行任务,corePoolSize也就是线程池中能够长期保持存活的数量(这里的长期存活指的是,即使没有要处理任务时依旧存在不被销毁的线程数)
  2. maxPoolSize:线程池有可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程有一个上线,这就是最大量maxPoolSize,也就是说在能执行的最大线程的数量就是maxPoolSize,再多就只能使用拒绝策略了。
  3. keepAliveTime:如果线程池当前的线程多余corePoolSize,那么如果多余线程空闲时间超过keepAliveTime,他们就会被终止,也就是把超过核心线程数的线程回收了。
  4. ThreadFactory:线程工厂,新的线程是由ThreadFactory创建的,默认使用Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级并且都不是守护线程。如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否守护线程等。
  5. runnableTaskQueue(任务队列):用于保存等待执行的任务阻塞队列。可以选择以下几个阻塞队列。

ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序。
LinkedBlockingQueue:一个链表结构的无界阻塞队列,也是按FIFO原则排序。
SynchronousQueue:一个不存储元素的阻塞队列,没个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常高于LinkedBlockingQueue。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列

  1. RejectedExecutionHandler(饱和策略):当队列或线程池都满了,说明线程池处于饱和状态,那么必须采用一种策略处理提交的新任务。这个策略默认是AbortPolicy,表示无法处理新任务抛出异常。常用的有以下4中策略:
    (1).AbortPolicy:直接抛出异常
    (2).CallerRunsPolicy:只用调用着所在线程来运行任务。
    (3).DiscardOldestPolicy:丢弃队列里最近一个任务,并执行当前任务。
    (4).DiscardPolicy:不处理,直接丢弃。

3.2线程池添加线程规则

在这里插入图片描述
5. 如果线程数小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来运行新任务。
6. 如果线程数等于(或大于)corePoolSize但少于maximumPoolSize,则将任务放入队列。
7. 如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程来运行任务
8. 如果入队列已满,并且线程数大于或等于maxPoolSize,则拒绝该任务

因此过于是否需要增加线程的判断顺序是:corePoolSize > workQueue > maxPoolSize

我们下面举个例子,假如线程池核心线程数为5,最大线程数为10,队列为100,他的请求过程是怎么样的呢?
首先线程池会先去创建线程使得数量到达核心线程数5,然后后续来的任务被添加到队列中,直到队列长度达到100。此时队列已满,将创建新的线程至线程数到达最大线程数量10,如果后面再来任务,则使用拒绝策略。所以这个线程池最大的任务请求数其实是10+100。

我们可以通过对线程池参数进行特殊的赋值,让线程池有不同的能力,下面我们说说增减线程的特点。

3.2.1 增减线程的特点
  1. 通过设置corePoolSize和maximumPoolSize
  2. 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它。
  3. 通过设置maximumPoolSize为很高的值,例如Integer.MAX_VALUE,可以允许线程池容纳任意数量的并发任务(这是危险的)
  4. 只有在队列填满时才创建多于corePoolSize的线程,所以如果使用的是误解队列(如LinkedBlockingQueue),那么线程数就不会超过corePoolSize。

3.3 线程池应该手动创建还是自动创建

我们先通过对自动创建的利弊进行分析,自动创建通常是使用Executors的静态方法直接尽行构建,他有以下几种线程池种类

  1. newFixedThreadPool:由于传进去的LinkedBlockingQueue是没有容量上限的,所以当请求数越来越多,并且无法及时处理完毕的时候,也就是请求堆积的时候,会最容易造成占用大量的内存,可能会导致OOM
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  1. newSingleThreadExecutor:可以看出,这里和newFixedThreadPool的原理基本一样,只不过把线程数直接设置成了1,所以这也会导致同样的问题,也就是当请求堆积的时候,可能会占用大量的内存。
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  1. newCacheThreadPool:可缓存的线程池,无界线程池,可以自动回收多余线程。这里的弊端在于第二个参数maximumPoolSize被设置为了Integer.MAX_VALUE,这可能会创建数量非常多的线程,甚至导致OOM。
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
  1. ScheduledThreadPool:定时线程池,可以根据定时执行任务。
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }

3.4 线程池里的线程数量设定为多少

根据上述的自动创建的线程池,其实并不能满足我们大部分的需求,甚至有的会引起线上问题,因此,我们可以针对性的对我们的业务手动创建线程池,那问题来了,该如何创建线程池,创建时我们需要对线程池的线程数量设置多少合适呢?
这里分为两种场景,一种是CPU密集型和IO密集型,他们的区别是一个是针对于CPU的调度一个是对磁盘、网卡等的读写。而我们针对于各自的场景以及充分的提高CPU的效率做出以下配置建议。

  • CPU密集型:通常用于加密、计算等,最佳线程数是CPU线程数的1-2倍左右。
  • IO密集型:读写数据库、文件、网络读写等,一般会大于cpu核心数很多倍,以JVM线程监控繁忙时间为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法:
  • 线程数=CPU核心数*(1+平均等待时间/平均工作时间)
    也就是说如果是CPU密集型,对CPU的占用时间更长(工作时间),那么尽量让他的线程数更小。而如果是占用时间很小,可以频繁的调度,那么考虑使用密集型的线程数配置方法。

3.5 如何正确关闭一个线程池

线程池提供shutdown和shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设为STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设为SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法就会返回true。至于应该调用哪一种来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

总结一下:
shutdown() 执行后停止接受新任务,会把队列的任务执行完毕。
shutdownNow() 也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop。
两个方法都会中断线程,用户可自行判断是否需要响应中断。

shutdownNow() 要更简单粗暴,可以根据实际场景选择不同的方法。

我们也可以用以下方式来停止线程池

        long start = System.currentTimeMillis();
        for (int i = 0; i <= 5; i++) {
            threadPool.execute(new Job());
        }

        threadPool.shutdown();

        while (!threadPool.awaitTermination(1, TimeUnit.SECONDS)) {
            LOGGER.info("线程还在执行...");
        }
        long end = System.currentTimeMillis();
        LOGGER.info("一共处理了【{}】s", (end - start));

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值