浅谈 Java 线程池工作原理 时间: 2018-03-14 23:44 分类: JDK ####概述 Java 给我们提供了多种线程池的实现,程序员只管通过`Executors`类直接创建即可,但是其工作原理还是有必要知道的,昨天面试时就遇到了这个问题,由于以前只是大致了解过其原理,所以细节问题答得不是很好,比如线程池中保存线程的阻塞队列是有界还是无界的问题。所以今天特地重新翻看了一遍源码。 ####简单分析工作原理 关于上面有界无界问题。当时面试没有答好,直接说成有界的并且可以手动设置。当时面试的人也没有指出这个错误,唉。 关于有界无界关键还得看创建的是什么类型的线程池,`Executors`提供了如下的几个线程池的创建: * Executors.newFixedThreadPool * Executors.newWorkStealingPool * Executors.newSingleThreadExecutor * Executors.newCachedThreadPool * Executors.newSingleThreadScheduledExecutor * Executors.newScheduledThreadPool 在这里,关于上面的重载方法就不一一列举了,上面就是`Executors`提供的 6 种线程池的创建。 对于以上几种线程池各自的特点,有几个常用的看名字就能大致知道其特点,但要深入分析,也不是一时半会儿能解释完的,所以在此也不再赘述。 还是就最开始的有界无界问题先简单说下,`newFixedThreadPool`与`newSingleThreadExecutor`是有界的,`newCachedThreadPool`与`newSingleThreadScheduledExecutor`、`newScheduledThreadPool`是无界的,这些都是查看 JDK 源码可以看到的,所以回答上面那个问题,只能说其中有些是有界的,比如固定大小的线程池、单线程池,有些是无界的,比如缓存线程池、定时周期执行任务的线程池。 最后,关键还是分析下线程池的工作原理。 在分析工作原理之前,首先说下自己以前对线程池理解的误区:以前以为线程池就是可以控制池大小(即同时执行线程的数量)的一个工具,内部还是使用`Thread.start()`来启动线程的,只是控制了线程的总数量。 上面的理解存在很大的问题,若是如上解释,那么线程池将没有体现任何优势,其实线程池的优势,就是减少了线程频繁创建的开销,下面来分析下说如何来减少线程频繁创建开销的。 在线程池中包含两个主要的成员变量: * `private final BlockingQueue workQueue;` * `private final HashSet workers = new HashSet();` 第一个变量,很容易想到,它就是用来保存要执行的线程任务的,是个阻塞队列。 第二个变量,保存的是实际工作的所有线程,此线程非用户提交的任务线程,下面说下`workers`的作用。 当用户通过`submit`或者`execute`提交任务时,会做一系列的判断,判断后可能会拒绝添加,如果能够添加的话就会调用`addWorker`方法,该方法主要做的工作就是,先判断当前工作线程是否超过`corePoolSize`线程池的大小,如果没有超过,则添加一个 `Worker` 线程并执行它,`Worker` 是线程池的一个内部类,若超过了则直接返回 false。 在``addWorker``之后,这时才会通过`workQueue.offer(command)`把需要执行的任务添加到阻塞队列。 下面详细解释下`Worker`线程的作用,其实现了`Runnable`接口,因此是个线程类,主要查看它的`run`方法: ```java /** Delegates main run loop to outer runWorker */ public void run() { runWorker(this); } final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { //循环从阻塞队列中获取任务线程,getTask()就是通过workQueue.poll或者workQueue.take获取任务线程 while (task != null || (task = getTask()) != null) { 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 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); } } ``` 看到上面代码,此时应该大致了解了线程池的工作原理: * 线程池中维护了一个 HashSet workers 集合,这个集合中才是真正后台一直执行的线程。 * Worker 线程类的 run 方法是一个死循环(准确来说不一定是的,根据线程池类型而定),这里暂且把它看成死循环好理解一些,循环中一直获取我们通过`submit`或者`execute`提交的线程任务。 * 关键地方看 29 行代码,获取到线程任务是调用的 run 方法,并非 start 方法,这也就是为什么线程池能够减少频繁创建线程开销的原因。 ####总结 其实线程池的工作原理,我们可以把它看成一个生产者与多个消费者的关系,即一个生产者通过`submit`或者`execute`生产线程,多个`worker`(即消费者)从阻塞队列里消费线程,关键点在于这里的消费是通过`run`方法消费。 标签: 线程池