八、线程池

线程池

【1】为什么需要线程池?线程池的优点?

1、为什么要使用线程池?

因为创建和销毁线程都是需要时间的,特别是需要创建大量线程的时候,时间和资源的消耗是不可忽略的,而合理的使用线程池中已经创建的线程,可以减少创建和销毁线程而花费的时间和资源。

2、线程池的优点?

(1)降低资源消耗:通过线程的重用可以降低创建和销毁线程花费的时间和资源;

(2)提高响应速度:任务到达时,因为利用线程池中已经创建好的线程,可以不用等待线程创建而直接执行任务;

(3)提高线程的可管理性:线程池允许我们开启多个任务而不用为每个线程设置属性(便于管理);线程池根据当前在系统中运行的进程来优化线程时间片(调优);线程池可以限制创建线程的数量,如果无限制的创建线程,不仅会消耗资源,还会降低系统的稳定性;

【2】线程池的工作原理

当向线程池提交一个任务以后,线程池处理任务的流程大概如下:

1、判断核心线程池是否满?如果没有,那么创建线程并执行任务;如果满,那么进入2;

2、判断阻塞队列(BlockingQueue)是否满?如果没有,那么把任务添加到阻塞队列中;如果满,进入3;

3、判断当前线程数是否大于最大线程数(maximumPoolSize)?如果没有,那么创建线程并执行任务;如果满,那么进入饱和策略(RejectionExecutionHandler);

4、如果核心线程池的线程执行完当前任务,那么立刻从阻塞队列头取任务来执行,如果队列为空,且当前线程超过了存活时间(keepAliveTime),那么判断当前线程数是否大于核心线程池的最大数(corePoolSize)?如果是,那么销毁当前线程,如果不是则保留当前线程。因此当创建的线程数大于核心线程池的最大数(corePoolSize),在所有任务执行完毕以后,线程池最终线程数量会回到corePoolSize。

流程图如下所示:

img

【3】线程池的创建

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:核心线程池大小。如果调用了prestartAllCoreThread()方法,那么线程池会提前创建并启动所有基本线程。

(2)maximumPoolSize:线程池大小

(3)keepAliveTime:线程空闲后,线程存活时间。如果任务多,任务周期短,可以调大keepAliveTime,提高线程利用率。

(4)timeUnit:存活时间的单位,有天(DAYS)、小时(HOURS)、分(MINUTES)、秒(SECONDS)、毫秒(MILLISECONDS)、微秒(MICROSECONDS)、纳秒(NANOSECONDS)

(5)runnalbleTaskQueue:阻塞队列。可以使用ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue

静态工厂方法Executors.newFixedThreadPool( )使用了LinkedBlockingQueue;

静态工厂方法Executors.newCachedThreadPool( )使用了SynchronousQueue;

(6)handler:饱和策略的句柄,当线程池满了的时候,任务无法得到处理,这时候需要饱和策略来处理无法完成的任务,饱和策略中有4种处理策略:

AbortPolicy:这是默认的策略,直接抛出异常;

CallerRunsPolicy:只是用调用者所在线程来运行任务;

DiscardOldestPolicy:丢弃队列中最老的任务,并执行当前任务;

DiscardPolicy:不处理,直接把当前任务丢弃;

当然也可以自定义饱和处理策略,需要实现RejectedExecutionHandler接口,比如记录日志或者持久化不能存储的任务等。

【4】线程池的关闭

可以通过shutdown或者shutDownNow来关闭线程池。

原理:遍历线程池中的线程,逐个调用线程的interrupt()方法来中断线程,所以不响应中断的线程可能永远无法终止

(1)shutDown:把线程池的状态设置为SHUTDOWN,然后中断所有没有正在执行任务的线程,而已经在执行任务的线程继续执行直到任务执行完毕;

(2)shutDownNow:把当前线程池状态设为STOP,尝试停止所有的正在执行或者暂停的线程,并返回等待执行的任务的列表;

在调用了shutDown或者shutDownNow后,调用isShutDown()返回true;当所有任务都关闭后,调用isTerminaed()方法返回true。(注意关闭线程池和所有线程关闭是不同的)

【5】线程池的合理配置

(1)CPU(计算)密集型则线程数配置尽可能的少,比如NCPU+1。可以通过Runtime.getRuntime().avaliableProcessors( )方法获得当前设备的CPU数量;

(2)IO密集型需要配置尽可能多的线程数,比如2*NCPU,因为IO处理时线程阻塞的时间很长,导致CPU空闲时间很长,多一点线程可以提高CPU利用率;

(3)混合型任务:如果可以拆分,最好拆分成CPU密集型任务+IO密集型任务,只要这两个拆分后的任务执行时间相差没有太大,那么拆分后的吞吐量将高于串行执行的吞吐量,如果时间相差太大,就没有必要分解;

(4)优先级不同的任务:使用PriorityQueue作为阻塞队列。(如果一直有优先级高的任务进来,可能导致优先级低的任务无法执行)

(5)执行时间不同的任务:可以交给不同规模的线程池来执行;或者使用PriorityQueue作为阻塞队列,把执行时间短的任务优先级设置高一点,让时间短的任务先执行;

(6)建议使用有界队列,这样可以保证系统的稳定性,如果队列时无界的,那么一直有任务进来就一直往阻塞队列添加节点,可能导致内存溢出。

posted @ 2019-10-21 21:18  ねぇ  阅读(...)  评论(... 编辑 收藏