人人都能看懂的图解java.util.concurrent并发包源码系列 ThreadPoolExecutor线程池

ThreadPoolExecutor的作用

  • 线程池是java.util.concurrent并发包里面非常重要的一个组件,它提供了线程复用的能力,避免重复地创建和销毁线程
  • 因为服务器的资源有限,每创建一个线程,都要在内存中分配一定的空间去存放线程Thread对象的数据,如果服务器并发访问量很大的话,每来一个请求就创建一个线程,那么服务器的内存资源很快就会耗尽然后OOM,通过线程池进行线程复用,就可以达到控制服务器线程数的效果
  • 其次创建线程是需要耗费时间的,首先要在内存中创建一个Thread对象,其次Java的线程是内核级线程,与操作系统线程一一对应,因此还要通过系统调用,创建一个内核级线程,这样就会降低了请求处理的速度,而通过复用线程池中的线程,就可以节省创建线程的时间成本
    在这里插入图片描述

ThreadPoolExecutor的核心原理

ThreadPoolExecutor的核心组件:

  • 工作者线程(Worker),以及存放工作者线程的集合
  • 创建线程用的线程工厂(ThreadFactory)
  • 存放任务的阻塞队列(BlockingQueue)

而它们之间的关系可用一张图来表示:
在这里插入图片描述

线程池中的线程不是裸的Thread对象,而是被包装为Worker对象,每个Worker对象中都有一个Thread对象,而Worker对象又被保存在线程池中的一个集合中(HashSet<Worker> workers)

线程的创建则是由ThreadFactory线程工厂完成,我们在创建ThreadPoolExecutor时可以指定ThreadFactory,比如我们可以自定义线程工厂,为每个线程起一个带有业务意义的线程名,方便问题排查。

我们提交到线程池中的任务(Runnable对象),会被放入阻塞队列BlockingQueue(姑且认为是直接放入队列,但其实并没有那么简单),然后线程池中的线程就会从阻塞队列中拉取任务并执行

ThreadPoolExecutor中的任务执行流程

ThreadPoolExecutor中的任务执行流程如下图:
在这里插入图片描述

  1. 首先,用户线程调用ThreadPoolExecutor执行任务的方法(execute方法、submit方法)
  2. ThreadPoolExecutor首先判断核心线程数是否已满,如果没有满,则创建核心线程并执行该任务
  3. 如果核心线程数已满,则判断存放任务的阻塞队列有没有满,如果没有满,则把任务提交到阻塞队列
  4. 如果阻塞队列已经满了,则判断ThreadPoolExecutor此时的线程数是否已经达到最大线程数,如果没有到达最大线程数,则创建非核心线程并执行该任务
  5. 如果ThreadPoolExecutor已达最大线程数,则执行拒绝策略

这里出现了几个非常重要的概念:核心线程数、核心线程、最大线程数、非核心线程、拒绝策略。下面一一介绍。

核心线程数与核心线程

核心线程一旦在线程池中被创建,就会一直工作,如果此时没有任务,它就会挂起等待任务的到来,不会被销毁(但是也可以设置核心线程允许空闲超时销毁,ThreadPoolExecutor提供了参数可以设置),就像是工厂里面的核心员工。

而核心线程数就是用来控制线程池中的核心线程上限,当线程池中的线程数没有到达核心线程数时,每来一个任务就创建一个核心线程(即使此时线程池中有空闲线程),目的就是尽快让线程池中的线程数达到核心线程数。

最大线程数与非核心线程

非核心线程在存放任务的阻塞队列没有满的情况下,是不会创建的,任务队列里面的任务将会由核心线程来消费。只有当阻塞队列满了,核心线程消费不过来,才会临时创建非核心线程去消费,当非核心线程空闲超过一定时间,就会被销毁。这就好比工厂在业务高峰期临时雇用的兼职工。

非核心线程数就是最大线程数减去核心线程数。

举个例子,假如我们设置的核心线程数是4,而最大线程数是10,则如图:

在这里插入图片描述

拒绝策略

拒绝策略简单理解就是当阻塞队列满了,然后线程池也达到了最大线程数,该任务无法正常被处理时要执行的处理逻辑,比如抛异常。

ThreadPoolExecutor源码走读

下面来看一下ThreadPoolExecutor的源码。

常量

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

ctl是一个原子整形,同时记录了线程池的状态和线程池的当前线程数。高三位用于记录线程池状态,和剩下的29位用于记录线程池的当前线程数。

在这里插入图片描述

    // runState is stored in the high-order bits
    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;

上面这五个就是ThreadPoolExecutor的状态,下面用一幅图描述它的状态流转:

在这里插入图片描述

创建线程池后,会处于RUNNING状态,代表正常工作;如果调用它的shutdown()方法,就会进入SHUTDOWN状态,此时的线程池不再接收新任务,但是会处理完任务队列中的任务;如果调用它的shutDownNow()方法,就会进入STOP状态,线程池不再接收新任务,任务队列中的任务也不会执行,正在执行的任务会终止;当进入了SHUTDOWN或STOP状态,等任务队列清空,线程全部销毁后,会进入TIDYING状态,调用terminated()方法,该方法调用完毕后,就会进入TERMINATED状态。

成员变量

private final BlockingQueue<Runnable> workQueue;

这个就是存入可执行任务的阻塞队列,我们调用ThreadPoolExecutor的execute()方法或submit()方法传递的任务对象,会放入到该队列中,线程池中的线程会不断地从该队列中拉取任务并执行。

private final HashSet<Worker> workers = new HashSet<Worker>();

工作者线程集合,在ThreadPoolExecutor中创建的线程,会被包装成Worker对象,然后放入到这个workers集合中。

private volatile ThreadFactory threadFactory;

线程工厂,用于创建工作者线程。

private volatile RejectedExecutionHandler handler;

拒绝策略,当达到最大线程数,已无法再创建线程,并且任务队列已满,那么我们提交任务到线程池中是无法正常被处理的,此时就会执行拒绝策略。

RejectedExecutionHandler有四个实现类,如下图:
在这里插入图片描述

private volatile long keepAliveTime;

非核心线程的最长空闲时间,超过这个时间都没有任务,那么就会销毁该线程。

private volatile int corePoolSize;

核心线程数。

private volatile int maximumPoolSize;

最大线程数。

在这里插入图片描述

构造方法

    public ThreadPoolExecutor(int corePoolSize, // 核心线程数
                              int maximumPoolSize, // 最大线程数
                              long keepAliveTime, // 空闲时长
                              TimeUnit unit, // keepAliveTime空闲时长的时间单位
                              BlockingQueue<Runnable> workQueue, // 阻塞队列
                              ThreadFactory threadFactory, // 线程工厂
                              RejectedExecutionHandler handler) { // 拒绝策略
		//省略...
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

就是简单保存上面介绍过的成员变量。

Worker

    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {

		// 当前Worker对应的线程
        final Thread thread;

		// 新线程第一个执行的任务
        Runnable firstTask;

        Worker(Runnable firstTask) {
            setState(-1);
            this.firstTask = firstTask;
            // 创建Thread对象,构造参数Runnable就是Worker自身
            this.thread = getThreadFactory().newThread(this);
        }

        public void run() {
            runWorker(this);
        }
        
    }        

final Thread thread 就是Worker对应的线程,在Worker的构造方法中通过线程工厂创建,创建线程时传递的Runnable对象就是Worker自身。因此当Thread的start方法被调用时,就会执行Worker的run方法

Runnable firstTask 是我们提交进来的任务,当线程池要创建新线程时,该任务不会放入任务队列,而是作为新线程第一个执行的任务保存在Worker中。

而且这里可以看到Worker的构造方法中,通过 getThreadFactory() 获取线程工厂ThreadFactory,然后通过ThreadFactory的 newThread方法 创建Thread对象,而传递的Runnable参数就是Worker自身,因此线程启动的时候会运行Worker的run方法

execute方法

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
            
        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);
    }

上面这段代码就是ThreadPoolExecutor执行任务的主流程:

  1. 首先 if (workerCountOf(c ) < corePoolSize) 判断当前线程数是否小于核心线程数,workerCountOf(c )就是通过位运算拿到ctl整形的低29位,得到当前线程数。
  2. 如果小于核心线程数,则调用 addWorker(command, true) 创建核心线程执行当前任务,command就是我们提交进来的任务,true表示创建的是核心线程。
  3. 如果 if (workerCountOf(c ) < corePoolSize) 判断条件为false,表示当前线程数大于等于核心线程数,则执行 if (isRunning(c ) && workQueue.offer(command)) ,isRunning(c )判断线程池是否处于RUNNABLE状态,workQueue.offer(command) 尝试把当前任务放入阻塞队列,放入队列成功会返回true。
  4. 如果任务放入队列失败,则执行 else if (!addWorker(command, false)),addWorker(command, false) 就是创建非核心线程执行当前任务,false表示创建的是非核心线程。
  5. 如果创建非核心线程也失败,表示线程数已达线程池的最大线程数,而此时任务队列也满了,那么只能调用 reject(command) 执行拒绝策略

用一幅图概括上面的流程:

在这里插入图片描述

addWorker方法

接下来进入addWorker方法,看看里面的逻辑。

    private boolean addWorker(Runnable firstTask, boolean core) {
        // 省略一大段...

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
        	// 创建Worker对象
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
            	// 加互斥锁
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                   	
                   	// 获取当前线程池状态(通过位运算截取ctl的高三位)
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive())
                            throw new IllegalThreadStateException();
                        // Worker对象放入workers集合
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                	// 释放锁
                    mainLock.unlock();
                }
                if (workerAdded) {
                	// 启动当前线程
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            // 省略...
        }
        return workerStarted;
    }

上面就是创建新线程添加Worker的逻辑,核心流程如下:

  1. new Worker(firstTask) 创建Worker对象,当前提交的任务作为新创建线程第一个执行的任务firstTask保存到Worker对象中
  2. mainLock.lock() 加ReentrantLock互斥锁,防止多个Worker对象并发同时放入workers集合
  3. workers.add(w) Worker对象添加到workers集合
  4. mainLock.unlock() 释放锁
  5. t.start() 启动线程

在这里插入图片描述

runWorker方法

当线程启动之后,就会运行Worker里面的run方法,Worker的run方法会调用runWorker方法

	public void run() {
		runWorker(this);
	}

runWorker方法里面就是Worker线程启动后的处理逻辑。

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        // 拿到Worker中保存的第一个要执行的任务
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock();
        boolean completedAbruptly = true;
        try {
        	// getTask() 从阻塞队列中获取任务
            while (task != null || (task = getTask()) != null) {
                w.lock();
                
                // 省略...
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                    	// 执行任务
                        task.run();
                    } 
                    // 省略各种catch
                    finally {
                        // 省略...
                    }
                } finally {
                    task = null;
                    // 省略...
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

可以看到主体逻辑就是首先执行Worker保存的第一个需要执行的任务firstTask,然后不断地while循环通过 getTask() 方法从阻塞队列中获取任务task,然后调用 task.run() 方法执行该任务。

然后getTask()方法中的核心逻辑就是下面这一段:

    private Runnable getTask() {
    // 省略...
		Runnable r = timed ?
			// 非核心线程会调这个方法从阻塞队列中拉取任务,超时返回null
			workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
			// 核心线程会调这个方法从阻塞队列中拉取任务,没有任务一直阻塞
			workQueue.take();
    // 省略...
    }

可以看到就是从阻塞队列中获取任务,如果是核心线程,这里会调用 workQueue.take() 获取任务,如果没有任务会一直阻塞;如果是非核心线程,会调用 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 获取任务,如果超时还没有获取到,则返回null,那么外面的那个while循环就会退出,当前线程的run方法就会执行结束。

在这里插入图片描述

总结

以上就是ThreadPoolExecutor源码的核心流程,虽然省略了许多代码,只展示任务提交和执行、线程创建和运行等相关的源码,但是这些都是ThreadPoolExecutor的核心部分,我觉得掌握了这些,基本上可以说是理解了线程池的核心原理了。

下面用一张图来总结整个流程:

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值