Java 线程池

概述

在 java 多线程场景下,如果在每次需要使用线程的时候创建它,并且在线程使用结束后回收的话,无论是创建还是回收线程都给系统带来了很大的开销,需要的线程越多,创建回收线程所额外消耗的资源也就越多。在这种场景下,就有了线程池的概念:

提前创建好一部分线程,当需要使用线程的时候,从线程池中拿出来使用,使用完毕后放回线程池,这样就可以省去创建和回收线程的额外消耗。

关于线程池我们可以抽象的理解成这样:假设现在有一家搬家公司(公司为了节省开支没有固定员工),当有客户需要搬家时,就去市场上临时找搬运工,搬运工忙完之后又回归到市场。后来公司老总发现每次去市场找搬运工的开销很大,因此公司招几个固定员工,这里的固定员工就类似线程池中的线程,后面当有客户需求时,就首先让这部分固定员工搬运,如果固定员工不够用的话再去市场上寻找临时搬运工。


一、线程池

下面我们分别从以下8个方面了解线程池:

  1. 线程池的七大参数:
  2. 线程池的五种状态:
  3. 线程池的五大种类:
  4. 线程池的四大特性:
  5. 线程池的四种阻塞队列:
  6. 线程池的四种拒绝策略:
  7. 线程池工作的原理:
  8. 线程池的关闭

1、线程池的参数

无论我们用什么方式创建线程池,最终都会走到如下构造方法:

public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler)

corePoolSize:线程池中核心线程的数量;当提交一个任务到线程池时,线程池会创建一个核心线程执行该任务,即使现在线程池中有其他空闲线程,直到核心线程数达到 corePoolSize 才不在创建,此时线程池会把该任务放到阻塞队列中。如果调用线程池的 preStartAllCoreThreads 方法,那么核心线程会在线程池初始化的时候创建。

maximumPoolSize:线程池中最大线程的数量,如果阻塞队列已满,并且当前线程数低于 maximumPoolSize 的话,就会创建新的线程执行任务。

keepAliveTime:线程最大空闲保持时长,指的是工作线程空闲之后持续的时间,达到该时长的工作线程将会被回收。默认情况下,这个参数只有线程数大于 corePoolSize 的时候才会起作用,一直回收到当前线程数等于 corePoolSize 。如果调用 allowCoreThreadTimeOut 方法的话,基于当前线程数低于 corePoolSize  也会回收线程,直到线程数等于0。

unit:参数 keepAliveTime 的时间单位。

workQueue:阻塞队列,当线程池中线程数量等于 corePoolSize 时,新来的任务会被放到阻塞队列中,关于阻塞队列的详细介绍在下面模块五详细说明。

threadFactory:用于创建线程的线程工厂。

handler:当阻塞队列已满,并且线程池中线程数等于 maximumPoolSize 的话,新来的任务必须通过一种策略进行处理,这里的 handler 就是线程池饱和状态下的策略关于拒绝策略的详细介绍在下面模块六详细说明


2、线程池的状态

关于线程池的状态,代码中通过以下五个常量来表示:

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

每个状态的工作方式如下所示:

  • RUNNING:可以接受任务,并且可以处理阻塞队列中的任务。
  • SHUTDOWN:不能接受任务,可以处理阻塞队列中的任务。
  • STOP:不能接受任务,不能处理阻塞队列中的任务,同时中断正在处理的任务。
  • Tidying:属于过渡阶段,在这个阶段表示所有的任务已经执行结束了,当前线程池中是不存在有效的线程的,并且将要调用terminated 方法。
  • TERMINATED:终止状态

线程池中这五种状态的转换关系在代码注释中已经有详细的介绍:

* RUNNING -> SHUTDOWN
*    On invocation of shutdown(), perhaps implicitly in finalize()
* (RUNNING or SHUTDOWN) -> STOP
*    On invocation of shutdownNow()
* SHUTDOWN -> TIDYING
*    When both queue and pool are empty
* STOP -> TIDYING
*    When pool is empty
* TIDYING -> TERMINATED
*    When the terminated() hook method has completed

具体转换策略如下图所示:


3、线程池的种类

3-1、SingleThreadPool

public static ExecutorService newSingleThreadExecutor() {
    return new Executors.FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>()));
}
  • 核心线程数和最大线程数都是1
  • 阻塞队列是无界队列

创建只有一个核心线程的线程池,主要作用是保证任务的顺序执行。即使在执行某个任务过程中因为出错线程停止,线程池也会创建新的线程保证任务的顺序执行,并保证任何时刻只有一个线程工作。

3-2、newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(),
            threadFactory);
}
  • 核心线程数和最大线程数相同
  • 阻塞队列是无界队列

创建一个最大线程数固定且都是核心线程的线程池,主要作用是可以很好的控制并发

3-3、newCachedThreadPool 

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
}
  • 没有核心线程,最大线程数为 MAX_VALUE

创建一个没有核心线程的线程池,理论上可以有 MAX_VALUE 个工作线程,并且默认情况下,空闲时间达到 60S 的线程将会被回收。主要作用是应对任务不规律的场景,假如长时间都没有任务,那么所有工作内存都会被回收,节省内存,当任务比较多的时候也可以创建大量的工作线程进行处理

3-4、ScheduledThreadPool

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new ScheduledThreadPoolExecutor.DelayedWorkQueue());
}
  • 有固定核心线程数,最大线程数为 MAX_VALUE
  • 任务队列会根据任务延时时间的优先级进行执行

创建一个固定核心线程数的线程池,场景有点类似 概述 中搬家公司的例子。它的阻塞队列可配置定时或者延时执行任务。主要作用是可以做定时器,并且在没有任务时,保证线程池中只有核心线程,此时如果有任务需要执行可以直接使用核心线程执行,当需要执行任务的填满阻塞队列时,再创建工作线程,减少资源耗费。

3-5、SingleThreadScheduledPool

public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
    return new Executors.DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1, threadFactory));
}
  • 有且只有一个核心线程
  • 可定时或延时执行任务

创建只包含一个核心线程的线程池,和 SingleThreadPool 的区别是,它可以做定时或延时任务,主要作用是做定时器


4、线程池的特性

无论哪种线程池都满足以下特性,这些特性也是线程池执行的规则:

  1. 当线程池中的线程数小于 corePoolSize 时,无论是否有空闲线程,创建新线程执行任务
  2. 当线程池中线程数大于等于 corePoolSize 时,无论是否有空闲线程,任务首先进入阻塞队列
  3. 当阻塞队列已满,并且线程池中线程小于 maximumPoolSize 时,创建新的工作线程执行该任务
  4. 当阻塞队列已满,并且线程池中线程等于 maximumPoolSize 时,对于新加入的任务,按照拒绝策略执行。


5、线程池的阻塞队列

在不同的线程池中需要使用不同的阻塞队列,就比如在 ScheduledThreadPool 中,如果使用没有界限的阻塞队列,那么就永远无法创建工作线程,也就失去了它的意义,下面我们介绍几种线程池中常用的阻塞队列:

5-1、SynchronousQueue

  • 阻塞队列的默认选项,不存储元素的阻塞队列
  • 将任务直接提交给线程,如果当前没有可用空闲线程,就创建新的线程

线程池 CachedThreadPool 默认使用,理论上它可以创建 MAX_VALUE 个线程,因此阻塞队列对于任务可以直接提交。

5-2、ArrayBlockingQueue

  • 基于数组的有界阻塞队列

默认的线程池构造中没有使用该阻塞队列,如果不考虑定时或者延时执行任务,ScheduledThreadPool 可以使用该阻塞队列。因为该队列基于数组实现,对于频繁的删除修改节点,不建议使用该阻塞队列。

5-3、LinkedBlockingQueue

  • 基于链表实现的无界阻塞队列

线程池  newFixedThreadPool 、 SingleThreadPool 默认使用该阻塞队列。因为链表的原因,可以快速按顺序执行,并且因为无界,所以理论上所有任务都可以被处理,缺点是当任务处理的速度小于任务加入队列的速度时,可能导致阻塞队列越来越长,最终导致后序所有请求都无法被及时处理。

5-4、DelayedWorkQueue

  • 基于优先级的有界阻塞队列

线程池ScheduledThreadPoolSingleThreadScheduledPool 默认使用该阻塞队列。主要用来做定时以及延时执行,其中该队列通过堆实现


6、线程池的拒绝策略

线程池的拒绝策略是自定义的,其中线程池系统为我们提供了以下4种常见的拒绝策略:

AbortPolicy:直接抛出异常

DiscardPolicy:直接丢弃该任务

DiscardOldestPolicy:丢弃阻塞队列中最近(队列头部)的任务,并尝试执行该任务,如果执行失败,继续丢弃。

CallerRunsPolicy:使用调用线程池的任务来执行该任务


7、线程池的工作原理

关于线程池的工作原理,主要从线程池执行一次任务的全流程说起:

线程池有 submit() 以及 execute() 两种方法来执行任务,其中  submit() 方法可以通过 Future 对象获取线程执行完的返回值,而 execute() 方法不行。其中 submit() 方法中会调用 execute() 方法,因此我们主要看 execute() 方法的执行策略。

根据上面的介绍,我们也可以知道:execute() 方法执行过程中如果出现异常,异常会在线程池内部被消耗。而 submit() 方法则可以抛出异常,通过返回值将异常抛出到外面。 

根据上面线程池特性的介绍,在线程池执行 execute() 方法时会有以下三种情况:

int c = ctl.get();
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);
    else if (workerCountOf(recheck) == 0)
        addWorker(null, false);
}
else if (!addWorker(command, false))
    reject(command);

情况一 :如果当前线程数量小于 corePoolSize :直接调用 addWorker() 方法创建一个新的 Worker对象 来执行我们当前的任务。

情况二:如果当前线程数量大于等于 corePoolSize 并且线程状态处于 RUNNING,那么我们首先将该任务放入阻塞队列中,并且再次检查线程池的状态,如果线程池不在 RUNNING 状态的话,就会回滚刚才添加到队列的操作,并且放弃该任务。如果处于 RUNNING 状态,那么检查当前工作线程数量是否等于0,如果等于0则创建一个线程执行该任务

情况三:如果我们无法将该任务排队,就尝试添加一个新线程处理该任务,如果失败,就抛弃该任务。

看到这里,可能会疑惑,为什么没有出现 maximumPoolSize 变量,这里是因为 maximumPoolSize 变量的判断其实都写在了addWorker() 方法中。

retry:
for (;;) {
    int c = ctl.get();
    int rs = runStateOf(c);

    // Check if queue empty only if necessary.
    if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
                    firstTask == null &&
                    ! workQueue.isEmpty()))
        return false;

    for (;;) {
        int wc = workerCountOf(c);
        if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
            return false;
        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
    }
}

这里我们可以看到,在添加 Worker对象 前,首先判断当前线程池的状态是否允许添加线程,然后判断当前线程的工作线程数是否已经超过 maximumPoolSize 变量,如果没有超过时,才尝试修改线程数量标识,如果修改成功就跳出循环。

看到这里大家也应该可以理解了,一个 Worker对象 实际上就是一个工作线程,下面我们来看 Worker对象 的实现。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    Worker(Runnable firstTask) {
        setState(-1); 
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
}

 上述是我截取之后的部分 Worker对象 的源码,其中 firstTask 属性就是我们在 addWorker() 方法中传过去的任务,在获取到任务之后,通过线程池构造方法中的 threadFactory 创建线程,其中该线程所执行的就是我们传过去的任务。

这里可能有点饶,我大概给大家介绍一下,java 创建一个线程常见有三种方法,我这里主要介绍两种

  • 继承 Thread 类,重写 run() 方法,该方法就是线程的执行体
  • 实现 Runnable 接口,重写 run() 方法,将该对象作为参数传递给 Thread对象

在我们调用 addWorker() 方法后,会调用 Worker 对象内部封装 Threadstart() 方法。

Worker w = null;
w = new Worker(firstTask);
final Thread t = w.thread;
t.start();

其中这里我省去了大量的判断逻辑,感兴趣的读者可以试着阅读源码。这里内部 thread 参数就是通过上述方法二,把 Worker对象作为参数创建的,执行它的 start() 方法,也就是调用 Worker对象run() 方法。

public void run() {
    runWorker(this);
}
final void runWorker(ThreadPoolExecutor.Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                            runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                wt.interrupt();
            try {
                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 {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

这里我们看到在 Worker对象 run() 方法会调用 runWorker() 方法,在该方法中首先判断任务是否为空,并且在执行之前还会判断一次当前线程池的状态,如果是 STOP 状态,会试着停止这些线程。如何检验没有问题,最终会通过 task.run() 这一步执行提交的任务。看到这里我们大概就了解了 execute() 方法的执行过程。

关于上面的 runWorker() 方法其实还有一个细节没有注意:当 firstTask 为空时,会通过 getTask() 方法从阻塞队列中获取任务。这也是为什么线程池可以复用线程的原因,通过修改 Runnable属性 来做到同一个线程执行不同的任务。以下这几种情况 getTask() 方法会返回空:

  • 工作线程数超过 maximumPoolSize
  • 线程池处于 STOP 状态
  • 线程池处于 SHUTDOWN 状态,并且队列为空
  • 工作线程已经超过最大等待时长 

工作线程会一直尝试从缓存队列中获取任务,直到无法取出任务时,就会跳出循环执行 processWorkerExit() 方法。执行该方法会给当前 Worker对象 一个标识,通过该标识告诉该对象需要回收,在回收过程中会检查线程池的状态,如果线程池处于停止状态并且阻塞队列和工作线程都为空,那么就将线程池状态过渡到 Tidying ,之后调用 terminated() ,将线程池彻底终止。

最后我们再试着看看 submit() 方法的源码:

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

 实际上, submit() 方法也是通过调用 execute() 方法来执行任务,不同的点是它传的任务是 RunnableFuture 接口类型 ,其中RunnableFuture 接口继承 Runnable 接口,它内部通过继承 Future接口 来获取任务返回值。


8、线程池的关闭

执行 shutdown() 方法,线程池不再处理新添加的任务,只执行阻塞队列中的任务,在执行阻塞队列任务的同时,回收空闲的 Worker对象,在等待阻塞队列中所有任务执行完毕后,并且所有 Worker对象 被回收后,将线程池状态过渡到 Tidying ,之后调用 terminated() ,将线程池彻底终止。

或者说直接执行 shutdownnow() 方法,线程池不再处理新添加的任务,并且尝试停止当前正在运行的线程,并且将阻塞队列中所有未执行的线程取出来,作为返回值返回,也就是说 shutdownnow() 方法会返回还没有执行的阻塞队列中的任务。当所有 Worker对象 被回收后,将线程池状态过渡到 Tidying ,之后调用 terminated() ,将线程池彻底终止。

awaitTermination(long timeout, TimeUnit unit) 方法阻塞的判断线程池是否处于 TERMINATED 状态,当处于 TERMINATED 状态时返回 TRUE,其中该方法的参数表示最长等待时间,如果达到该时间还没有处于 TERMINATED 状态则返回 FALSE

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值