彻底学习线程池

322 阅读11分钟

1.什么是线程池

1.1 为什么要使用线程池

创建/销毁线程需要消耗系统资源,因为Hotspot虚拟机中每个线程对应操作系统的一个线程,线程池可以复用已创建的线程。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。线程池可以对线程做统一管理。

1.2 使用线程池的好处

合理利用CPU和内存,复用线程,统一管理。

2.创建和停止线程池

2.1 线程池构造函数的参数

首先先搞清楚我们经常所说的线程池到底是指哪个类?
知道大家见过很多名字类似的类(Executor、ExecutorSerive等),但可以确定的是ThreadPoolExecutor,后面也会介绍其他与它相关的类及关系。先来看线程池的构造方法:

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

ThreadPoolExecutor总共有四个构造函数,这是两个核心的构造函数。其中一个是5个参数的构造函数,一个是7个参数的构造函数。最根本的其实就一个构造函数,也就是7个参数的这个,包含了5个必须的参数和2个非必须的参数(非必须的意思是其他构造函数会传默认值),另外两个构造方法只是这两个非必须参数不同。

2.1.1 五个必须的参数

int corePoolSize:该线程池中核心线程数最大值。核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干(铁饭碗),而非核心线程如果长时间的闲置,就会被销毁(临时工)。
int maximumPoolSize:该线程池中线程总数最大值 。该值等于核心线程数量 + 非核心线程数量。
long keepAliveTime:非核心线程闲置超时时长。非核心线程如果处于闲置状态超过该值,就会被销毁。
TimeUnit unit:keepAliveTime的单位。
BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象。
有3种常见的队列类型:
1.直接队列
SynchronousQueue。同步队列,内部容量为0,每个put操作必须等待一个take操作。
2.无界队列
ArrayBlockingQueue。链式阻塞队列,底层数据结构是单向链表,默认大小是Integer.MAX_VALUE。
3.有界队列
ArrayBlockingQueue。数组阻塞队列,底层数据结构是循环数组,需要指定队列的大小。

2.1.2 两个非必须的参数

ThreadFactory threadFactory:创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。
RejectedExecutionHandler handler:拒绝处理策略,阻塞队列已经满了并且线程数量大于最大线程数就会采用拒绝处理策略,四种拒绝处理的策略为 :
a.ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
b.ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
c.ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
d.ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

2.3 正确创建线程池的方法

阿里巴巴代码规约提到,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。因为Executors各个方法是有弊端的,如果不清楚就随便使用可能会导致OOM。。
Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  用的是无界阻塞队列,可能会导致堆积的请求处理队列会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

创建完成后,可以调用execute或submit方法执行任务,这两个方法的区别在于execute没有返回值并且只能传Runnable,而submit方法可以传Runnable和Callable并且有返回值Future。submit内部其实还是调用了execute方法。

2.2 线程池的处理流程

1.线程总数量 < corePoolSize,无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到corePoolSize,在核心线程数量 < corePoolSize时)。
2.线程总数量 >= corePoolSize时,新来的线程任务会进入任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执行(体现了线程复用)。
3.当缓存队列满了,说明这个时候任务已经多到爆棚,需要一些“临时工”来执行这些任务了。于是会创建非核心线程去执行这个任务。
4.缓存队列满了, 且总线程数达到了maximumPoolSize,则会采取上面提到的拒绝策略进行处理。

2.3 停止线程池的正确方法

2.3.1 shutdown和isShutdown以及isTerminated

shutdown不会马上停止,会执行完线程池中正在运行的任务和阻塞队列中等待的任务,但是不能接受新提交的任务,否则会抛异常。如果调用了shutdown,isShutdown就返回true,仅仅是标志位的判断。 isTerminated才是代表真正的停止,运行中的任务及队列中的任务都清空了才返回true。

2.3.2 awaitTermination

其实和关闭没有什么关系,它只是用来检测一段时间内线程池是否完全执行完毕的一个方法,返回true或false。

2.3.3 shutdownNow

会直接停止线程池,向运行中的线程发出interrupt中断请求,阻塞队列中的任务会直接返回,也就是说shutdownNow是有返回值的,返回未执行的任务列表。

3.常用线程池的特点和用法

Executors类中提供的几个静态方法来创建线程池。也就是说JDK已经帮我们封装了一些常用的线程池,但是我们使用这些封装好的线程池要小心,可能会OOM,所以应该结合自己的业务场景去使用。(最好手动创建)

3.1 newCachedThreadPool

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

可以看到corePoolSize为0,不创建核心线程,线程池最大为Integer.MAX_VALUE,SynchronousQueue是一个内部只能包含一个元素的队列。也就是说,newCachedThreadPool来一个任务就创建一个非核心线程,可以理解为无限创建(线程池最大为Integer.MAX_VALUE),并且在线程空闲60秒的时候会被回收。

3.2 newFixedThreadPool

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

核心线程数量和总线程数量相等,都是传入的参数nThreads,所以只能创建核心线程,不能创建非核心线程,所以keepAliveTime非核心线程闲置时长也就没有意义了,为0。LinkedBlockingQueue的默认大小是Integer.MAX_VALUE,是一个无界队列,所以如果核心线程空闲,则交给核心线程处理;如果核心线程不空闲,则入列等待,直到核心线程空闲。

相比CachedThreadPool,FixedThreadPool只会创建核心线程,而CachedThreadPool只会创建非核心线程。由于核心线程不会被回收,所以没有任务的情况下, FixedThreadPool占用资源更多。另外,FixedThreadPool是因为阻塞队列可以很大(最大为Integer最大值),故几乎不会触发拒绝策略;CachedThreadPool是因为线程池很大(最大为Integer最大值),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。

3.3 newSingleThreadExecutor

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

SingleThreadPool和FixedThreadPool很类似,只是SingleThreadPool只有一个线程。如果这个唯一的核心线程不空闲,那么新来的任务会存储在任务队列里等待执行。

3.4 newScheduledThreadPool

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

//ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

创建一个周期性线程池,支持定时及周期性任务执行,DelayedWorkQueue是基于堆结构的等待队列 。ScheduledExecutorService有三个主要方法:
schedule:在给定delay后执行一次任务。
scheduleAtFixedRate:如果执行时间小于指定的间隔时间的情况下,callable或runnable每隔period执行一次,如果执行时间大于指定的间隔时间,每隔程序执行时间执行一次。
scheduleWithFixedDelay:不管执行时间怎么样,两次执行任务之间必须间隔delay时间。

3.5 newWorkStealingPool

WorkStealingPool是JDK1.8加入的。可以理解为每个线程都有一个等待队列,当某个线程处理完时,会从其他线程中窃取任务执行,是基于ForkJoinPool实现的,ForkJoinPool是用于执行ForkJoinTask任务的线程池。

public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

4.如何处理任务太多的情况

线程池的构造函数中有个拒绝策略的参数,也就是通过它来处理任务太多的情况的。

4.1 拒绝的时机

阻塞队列已经满了并且线程池已经达到最大线程数。

4.2 四种拒绝策略

ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

5.线程池的钩子方法

通过继承ThreadPoolExecutor类,重写beforeExecute和afterExecute方法可以在每个任务执行前后做一些事情,比如加日志等等。

6.线程池实现原理及源码分析

6.1 线程池相关类的关系与作用

先来看张UML图: Executor是一个接口,里面只有一个execute方法。ExecutorService也是个接口,继承了Executor,拓展了一些接口。AbstractExecutorService是一个抽象类,实现了ExecutorService接口。ThreadPoolExecutor也就是线程池继承了AbstractExecutorService类。而Executors其实就是一个工具类,主要封装了用来创建线程池的方法。

6.1 线程池状态

线程池也有自己的状态。ThreadPoolExecutor类中定义了一个volatile int变量runState来表示线程池的状态 ,分别为RUNNING、SHUTDOWN、STOP、TIDYING 、TERMINATED。
RUNNING:线程池创建后处于的状态。
SHUTDOWN:调用shutdown()方法后处于SHUTDOWN状态,线程池不能接受新的任务,会等待阻塞队列的任务完成。
STOP:调用shutdownNow()方法后处于STOP状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。
TIDYING:当所有的任务已终止,ctl记录的”任务数量”为0(ThreadPoolExecutor中有一个控制状态的属性叫ctl,它是一个AtomicInteger类型的变量。),线程池会变为TIDYING状态。接着会执行terminated()钩子函数。
线程池处在TIDYING状态时,执行完terminated()方法之后,就会由 TIDYING -> TERMINATED, 线程池被设置为TERMINATED状态。

6.2 任务复用原理(源码分析)

ThreadPoolExecutor在创建线程时,会将线程封装成工作线程worker,并放入工作线程组中,然后这个worker反复从阻塞队列中拿任务去执行。处理任务的核心方法是execute,我们看看 JDK 1.8 源码中ThreadPoolExecutor是如何处理线程任务:

// JDK 1.8 
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();   
    int c = ctl.get();
    // 1.当前线程数小于corePoolSize,则调用addWorker创建核心线程执行任务
    if (workerCountOf(c) < corePoolSize) {
       if (addWorker(command, true))
           return;
       c = ctl.get();
    }
    // 2.如果不小于corePoolSize,则将任务添加到workQueue队列。
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 2.1 如果isRunning返回false(状态检查),则remove这个任务,然后执行拒绝策略。
        if (! isRunning(recheck) && remove(command))
            reject(command);
            // 2.2 线程池处于running状态,但是没有线程,则创建线程
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 3.如果放入workQueue失败,则创建非核心线程执行任务,
    // 如果这时创建非核心线程失败(当前线程总数不小于maximumPoolSize时),就会执行拒绝策略。
    else if (!addWorker(command, false))
         reject(command);
}

可以看到addWorker才是关键:

private boolean addWorker(Runnable firstTask, boolean core) {
// 其余代码略.........
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        // 1.创建一个worker对象
        w = new Worker(firstTask);
        // 2.实例化一个Thread对象
        final Thread t = w.thread;
        if (t != null) {
            // 3.线程池全局锁
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 4.启动这个线程
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

创建worker对象,并初始化一个Thread对象,然后启动这个线程对象。接着看Worker类:

// Worker类部分源码
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    final Thread thread;
    Runnable firstTask;

    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    public void run() {
            runWorker(this);
    }
    //其余代码略...
}

Worker类实现了Runnable接口,所以Worker也是一个线程任务。在构造方法中,创建了一个线程,线程的任务就是自己。故addWorker方法调用addWorker方法源码下半部分中的第4步t.start,会触发Worker类的run方法被JVM调用。
我们再看看runWorker的逻辑,这里就是线程复用的核心:

// Worker.runWorker方法源代码
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    // 1.线程启动之后,通过unlock方法释放锁
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 2.Worker执行firstTask或从workQueue中获取任务,如果getTask方法不返回null,循环不退出
        while (task != null || (task = getTask()) != null) {
            // 2.1进行加锁操作,保证thread不被其他线程中断(除非线程池被中断)
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            // 2.2检查线程池状态,倘若线程池处于中断状态,当前线程将中断。 
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                // 2.3执行beforeExecute 
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 2.4执行任务
                    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 {
                    // 2.5执行afterExecute方法 
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                // 2.6解锁操作
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

worker通过while循环不断地调用getTask方法从阻塞队列中获取任务,去执行任务的run方法,从而达到复用线程的目的。只要getTask方法不返回null,此线程就不会退出。





参考资料:
《实战高并发编程》
《深入浅出Java多线程》