admin

浅谈 Java 线程池工作原理
概述Java 给我们提供了多种线程池的实现,程序员只管通过Executors类直接创建即可,但是其工作原理还是有必...
扫描右侧二维码阅读全文
14
2018/03

浅谈 Java 线程池工作原理

概述

Java 给我们提供了多种线程池的实现,程序员只管通过Executors类直接创建即可,但是其工作原理还是有必要知道的,昨天面试时就遇到了这个问题,由于以前只是大致了解过其原理,所以细节问题答得不是很好,比如线程池中保存线程的阻塞队列是有界还是无界的问题。所以今天特地重新翻看了一遍源码。

简单分析工作原理

关于上面有界无界问题。当时面试没有答好,直接说成有界的并且可以手动设置。当时面试的人也没有指出这个错误,唉。
关于有界无界关键还得看创建的是什么类型的线程池,Executors提供了如下的几个线程池的创建:

  • Executors.newFixedThreadPool
  • Executors.newWorkStealingPool
  • Executors.newSingleThreadExecutor
  • Executors.newCachedThreadPool
  • Executors.newSingleThreadScheduledExecutor
  • Executors.newScheduledThreadPool

在这里,关于上面的重载方法就不一一列举了,上面就是Executors提供的 6 种线程池的创建。
对于以上几种线程池各自的特点,有几个常用的看名字就能大致知道其特点,但要深入分析,也不是一时半会儿能解释完的,所以在此也不再赘述。

还是就最开始的有界无界问题先简单说下,newFixedThreadPoolnewSingleThreadExecutor是有界的,newCachedThreadPoolnewSingleThreadScheduledExecutornewScheduledThreadPool是无界的,这些都是查看 JDK 源码可以看到的,所以回答上面那个问题,只能说其中有些是有界的,比如固定大小的线程池、单线程池,有些是无界的,比如缓存线程池、定时周期执行任务的线程池。

最后,关键还是分析下线程池的工作原理。

在分析工作原理之前,首先说下自己以前对线程池理解的误区:以前以为线程池就是可以控制池大小(即同时执行线程的数量)的一个工具,内部还是使用Thread.start()来启动线程的,只是控制了线程的总数量。
上面的理解存在很大的问题,若是如上解释,那么线程池将没有体现任何优势,其实线程池的优势,就是减少了线程频繁创建的开销,下面来分析下说如何来减少线程频繁创建开销的。

在线程池中包含两个主要的成员变量:

  • private final BlockingQueue<Runnable> workQueue;
  • private final HashSet<Worker> workers = new HashSet<Worker>();

第一个变量,很容易想到,它就是用来保存要执行的线程任务的,是个阻塞队列。
第二个变量,保存的是实际工作的所有线程,此线程非用户提交的任务线程,下面说下workers的作用。

当用户通过submit或者execute提交任务时,会做一系列的判断,判断后可能会拒绝添加,如果能够添加的话就会调用addWorker方法,该方法主要做的工作就是,先判断当前工作线程是否超过corePoolSize线程池的大小,如果没有超过,则添加一个 Worker 线程并执行它,Worker 是线程池的一个内部类,若超过了则直接返回 false。
addWorker之后,这时才会通过workQueue.offer(command)把需要执行的任务添加到阻塞队列。
下面详细解释下Worker线程的作用,其实现了Runnable接口,因此是个线程类,主要查看它的run方法:

/** 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<Worker> workers 集合,这个集合中才是真正后台一直执行的线程。
  • Worker 线程类的 run 方法是一个死循环(准确来说不一定是的,根据线程池类型而定),这里暂且把它看成死循环好理解一些,循环中一直获取我们通过submit或者execute提交的线程任务。
  • 关键地方看 29 行代码,获取到线程任务是调用的 run 方法,并非 start 方法,这也就是为什么线程池能够减少频繁创建线程开销的原因。

总结

其实线程池的工作原理,我们可以把它看成一个生产者与多个消费者的关系,即一个生产者通过submit或者execute生产线程,多个worker(即消费者)从阻塞队列里消费线程,关键点在于这里的消费是通过run方法消费。

Last modification:March 14th, 2018 at 11:51 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment