为了账号安全,请及时绑定邮箱和手机立即绑定

juc-11-线程池1-如何创建线程池

标签:
Java

1、为什么要用线程池?

下面先看看不使用线程池的程序,每个任务都新开一个线程处理。

public class NoPool {

    public static void main(String[] args) {

        // 模拟程序需要执行1000个任务
        for (int i = 0; i < 1000; i++) {
            // 每个任务启动一条线程去处理
            new Thread(new Task(), "Thread" + i).start();
        }
    }

    static class Task implements Runnable {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "执行任务...");
        }
    }
}

运行结果:

Thread0执行任务...
Thread3执行任务...
Thread2执行任务...
Thread1执行任务...
Thread5执行任务...
Thread4执行任务...
...
Thread996执行任务...
Thread997执行任务...
Thread998执行任务...
Thread999执行任务...

从执行结果上看,每个任务启动一条线程来执行任务,都顺利完成任务,但是这其中隐藏了一些问题

  1. 过多的线程会占用太多的内存
  2. 每条线程都需要经过创建销毁这两个生命周期,大量的创建和销毁工作,会降低服务器的工作效率

2、ThreadPoolExecutor 线程池

每个任务创建单独的线程,会带来一些资源和效率方面的问题,这时JDK中的线程池 ThreadPoolExecutor 登场了,它就是来解决和管理线程的相关问题的:

  1. 用少量的线程来避免内存占用过多问题;
  2. 让这部分线程都保持工作,且可以反复执行任务——线程可以复用,避免创建销毁带来的开销,降低资源的消耗,提高响应速度;
  3. 执行任务的线程总量可控和提高线程的可管理性

ThreadPoolExecutor 的继承关系图:

2.1 构造函数的参数解释

为了让大家能够看懂下面的构造函数源码,这里先给大家介绍构造函数中各个参数的含义。

  • corePoolSize :int类型,核心线程数,线程池进行初始化的时候,默认情况下,线程池里没有任何线程,线程池会等待有任务到来的时候,再进行创建新线程去执行任务;
  • maxPoolSize :int类型,最大的线程数,线程池有可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一定的上限,就是最大的线程数 maxPoolSize;
  • keepAliveTime :long类型,线程空闲时的保持存活时间,如果线程池的当前的线程数多于了corePoolSize,那么超出 corePool 数量的线程,如果其空闲时间超过keepAliveTime,将会被终止;
  • workQueue :BlockingQueue类型,工作队列,也就是存储任务的阻塞队列,常见的 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue,我在前面的文章中有介绍过阻塞队列的使用。
  • threadFactory :ThreadFactory类型,当线程池需要新的线程的时候,会用ThreadFactory来生成新的线程,默认使用Exectors.defaultThreadFactory(),创建出来的线程都在一个线程组,拥有相同的 NORM_PRIORITY 优先级,并且都不是守护线程。如果自己指定 ThreadFactory,那么就可以改变 线程名、线程组、优先级、是否是守护线程等。通常我们使用 Exectors.defaultThreadFactory() 默认的线程工厂就可以了。
  • Handler :RejectedExecutionHandler类型,线程池无法接受你所提交的任务的时候,采取的拒绝策略

JDK中提供的4中拒绝策略 RejectedExecutionHandler :

  1. AbsortPolicy:直接抛出异常,默认策略
  2. DiscradPolicy:直接丢弃
  3. DiscardOldestPolicy:丢弃最老的任务
  4. CallerRunsPolicy:那个线程提交的任务,则由该线程进行运行,这样可以降低提交速度

2.2 线程池的构造函数

从下面的构造函数源码可以看到,所有的构造器都会调用:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
指定 corePoolSize、maximumPoolSize、keepAliveTime 、workQueue、threadFactory 和 RejectedExecutionHandler

  1. 如果没有指定线程工厂threadFactory,则默认使用Executors.defaultThreadFactory()
  2. 如果没有指定拒绝策略 RejectedExecutionHandler,则默认使用 AbortPolicy,表示直接抛出异常。

构造函数源码:


    // 线程池无法接受你所提交的任务的时候,默认采取的拒绝策略:AbortPolicy 直接抛出异常
    private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
    
    // 指定 corePoolSize、maximumPoolSize、keepAliveTime 和 workQueue
    // 使用默认的 threadFactory:Executors.defaultThreadFactory() 和 默认的RejectedExecutionHandler:AbortPolicy
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
    }

    // 指定 corePoolSize、maximumPoolSize、keepAliveTime 、workQueue 和 threadFactory
    // 使用默认的RejectedExecutionHandler:AbortPolicy
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,threadFactory, defaultHandler);
    }
    
    // 指定 corePoolSize、maximumPoolSize、keepAliveTime 、workQueue 和 RejectedExecutionHandler
    // 使用默认的 threadFactory:Executors.defaultThreadFactory()
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);
    }
    
    // 指定 corePoolSize、maximumPoolSize、keepAliveTime 、workQueue、threadFactory 和 RejectedExecutionHandler
    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;
    }

2.3 ThreadPoolExecutor 添加执行任务(Runnable)的步骤

    1. 如果线程小于corePoolSize的时候,即使线程有处于空闲状态,也会继续创建新的线程运行新的任务
    1. 如果等于大于corePoolSize,但是小于maxPoolSize,则将任务放入工作队列(阻塞队列),从队列中取任务时,如果当前有效的线程数目小于corePoolSize,使用 take() 方法获取任务,否则使用 poll(keepAliveTime, TimeUnit.NANOSECONDS) 获取任务;
    1. 如果队列已满,并且线程数小于maxPoolSize,创建新的线程来运行人任务。
    1. 如果队列已满,并且线程数大于或者等于maxPoolSize,则拒绝该任务,拒绝策略由 RejectedExecutionHandler决定。

3、Executors 创建线程池实例

Executors 提供了大量静态工厂方法,用于创建线程池实例,下面讲解几个常用的静态工厂方法。

3.1、static ExecutorService newFixedThreadPool(int nThreads)

Executors 中 newFixedThreadPool 的源码

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

newFixedThreadPool(int nThreads) :用于创建固定线程数的线程池。

  • corePoolSize 和 maximumPoolSize 一样,都等于入参 nThreads;
  • keepAliveTime 等于0,因为线程数永远不会大于 corePoolSize,所以这里keepAliveTime这个参数没有作用,设置为0;
  • workQueue 是 LinkedBlockingQueue 实例,通过 new LinkedBlockingQueue<Runnable>() 空参构造器创建实例,没有指定阻塞队列的容量,则队列的默认容量是 Integer.MAX_VALUE;

注意: 由于传进去的 LinkBlockingQueue 的容量是 Integer.MAX_VALUE,所以当请求数越来越多,并且无法及时处理完毕的时候,也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致 OOM。

3.2、static ExecutorService newSingleThreadExecutor()

Executors 中 newSingleThreadExecutor 的源码

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

newSingleThreadExecutor() :用于创建只有一条线程的线程池。

通过源码,可以看出,newSingleThreadExecutor() 和 newFixedThreadPool(int nThreads) 基本一样,不同的是在 newSingleThreadExecutor() 中 corePoolSize 和 maximumPoolSize 都固定等于 1 ,其它参数跟newFixedThreadPool(int nThreads) 一样;

注意: newSingleThreadExecutor() 也可能会导致跟 newFixedThreadPool(int nThreads) 同样的问题,也就是 LinkBlockingQueue 的容量是 Integer.MAX_VALUE,所以当请求数越来越多,并且无法及时处理完毕的时候,也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致 OOM。

3.3、static ExecutorService newCachedThreadPool()

Executors 中 newCachedThreadPool 的源码

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

newCachedThreadPool() :创建无界线程池,maximumPoolSize = Integer.MAX_VALUE 近似无界,,具有自动回收多余线程的功能。

  • corePoolSize = 0 说明核心线程数为0,
  • maximumPoolSize = Integer.MAX_VALUE,最大可创建 Integer.MAX_VALUE 个线程;
  • keepAliveTime 等于60s,因为核心线程数 corePoolSize 为0,所以只要线程池中的线程空闲超过 keepAliveTime 都会被自动回收;
  • workQueue 是 SynchronousQueue 实例,这是一个不存储元素的阻塞队列,它的容量为 0,每一个put操作都要等待一个take操作,也就是说如果当前线程池中所有线程都正在忙,则为该任务创建一个新线程并将这个放入池中。

注意: 第二个参数 maxPoolSize 被设置为了 Integer.MAX_VALUE,这可能会创建数量非常多的线程,甚至导致 OOM。

3.4、static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

Executors 中 newScheduledThreadPool 的源码

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

**ScheduledThreadPoolExecutor 构造器,指定核心线程数 corePoolSize **


public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {
...        
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
...    
}

newScheduledThreadPool(int corePoolSize) :创建支持延迟任务、周期性执行任务的线程池。

  • corePoolSize 通过入参 corePoolSize 指定;
  • maximumPoolSize = Integer.MAX_VALUE,最大可创建 Integer.MAX_VALUE 个线程;
  • keepAliveTime 等于0,如果线程池的当前的线程数多于了corePoolSize,那么超出 corePool 数量的线程一旦空闲,就被回收
  • workQueue 是 DelayedWorkQueue 实例,这个队列是无界队列,支持延时获取的元素的阻塞队列

3.4.1 ScheduledExecutorService 方法说明:

  • schedule(Runnable command, long delay, TimeUnit unit) 和 schedule(Callable callable, long delay, TimeUnit unit) :
    指定延迟时间,延时执行任务,只执行一次

  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) :
    周期性任务,提交固定时间间隔的任务,前后两个任务的起始时间间隔是固定的

  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) :
    周期性任务,提交固定延时间隔执行的任务,前一个任务结束时间到后一个任务开始时间的时间间隔是固定的

3.5 Executors 静态方法参数对比

Parameter FixedThreadPool CachedThreadPool ScheduledThreadPool SingleThreadPoolExecutor
corePoolSize 入参 nThreads 0 入参 corePoolSize 1
maxPoolSize 入参 nThreads Integer.MAX_VALUE 入参 corePoolSize 1
keepAliveTime 0 60 seconds 0 0

3.6 线程池手动创建还是自动创建

建议:手动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源耗尽 和 OOM(内存溢出) 的风险

4、线程池中的线程数量设定为多少比较合适?

  1. 计算密集型的
    CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的 1-2 倍左右。

  2. 耗时IO型的
    耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于cpu核心数很多倍,以jvm线程监控显示繁忙情况为依据。保证线程空闲可以衔接上。

Brain Goetz 推荐的计算方法

参考 Brain Goetz 推荐的计算方法:
线程数:cpu核心数 * (1+平均等待时间/平均工作时间)

这篇文章仅仅是介绍了ThreadPoolExecutor的构造方法,如何创建一个线程池,详细地说明了ThreadPoolExecutor的构造方法中各个参数的含义。接着又讲解了Executors 几个常用的静态工厂方法,用于创建线程池实例,朋友们可以更加业务需求去选择,但是我建议最好是通过 ThreadPoolExecutor 的构造函数去创建线程池,这样资源消耗可控性更强。

下篇文章介绍 ThreadPoolExecutor 和 Executors 创建的线程池的使用案例,以及ThreadPoolExecutor 中各个核心函数用法。

代码:
github.com/wengxingxia/002juc.git

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
11
获赞与收藏
8

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消