【多线程】线程复用的原理

邓敏 2021年12月27日 111次浏览

在了解线程复用原理之前,我们再回顾一下之前在线程池的初始配置详解的文章中提到的创建线程的流程
image

在线程提交线程任务的时候,线程池会提前判断线程池中执行的任务是否达到核心线程数了,如果达到了核心线程数那么他就会将这个多出来的任务塞入队列中,如果发现队列中的任务数也达到了上限,这个时候线程池就会开始在核心线程数的基础上创建更多的线程,当创建的线程达到了最大线程的线程数,并且线程队列也满了,这个时候就会触发我们设置的拒绝策略。

看到这里大家仔细思考一下,为什么线程池会设置一个核心线程数呢?这样做的好处有啥,其实这里就是为了满足线程复用的作用,在线程池中,我们提交给线程池的每个任务,在线程池中称之为worker,在线程池初始化之后,线程池就会创建我们设置的核心线程数大小的线程数,当有任务进来的时候,线程池其实并不会去新创建线程来执行刚刚进来的任务,而是初始化创建的线程中直接获取worker中的任务,执行他的run方法,当当前这个线程任务执行完成之后,就会再去无限循环的获取worker列表中的下一个任务,依次循环,从而达到了线程复用的目的。

知道了大概的流程,我们接下来再看看源码。
image.png

第一段代码

if (command == null) 
    throw new NullPointerException();

这段代码对线程任务做了一个非空判断,如果线程任务为null则会抛出空指针异常。

第二段代码

if (workerCountOf(c) < corePoolSize) { 
    if (addWorker(command, true)) 
        return;
        c = ctl.get();
}

workerCountOf()方法为获取当前正在运行的线程数,如果当前运行的线程数,小于核心线程数,这个时候就会添加一个线程任务到核心线程池中。
可以看到addWorker(command, true)方法中传入了一个true,这个布尔值代表我这次添加任务到线程池中需不需要创建额外的线程,ture就代表不需要创建额外的线程池,false就代表需要创建额外的线程。在当前这个情况,当前执行的任务的数量小于核心线程数,所以方法传入的是ture。

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);
}

如果代码执行到了这里,说明当前线程任务已经大于核心线程数了,那么这个时候就需要通过isRunning(c) && workQueue.offer(command)来检查线程池状态是否为Running,如果为Running就通过workQueue.offer(command)方法放入到队列中,如果通过(! isRunning(recheck) && remove(command)) 判断线程不在Running状态,说明线程池就已经关闭掉了,这个时候就会执行remove(command)方法来移除刚刚添加到队列任务中的任务,并通过reject(command)执行拒绝策略。
如果发现线程在执行状态下,这个时候就会通过workerCountOf(recheck) == 0来判断之前的线程有没有执行完成,如果之前的线程任务已经结束了这个时候workerCountOf(recheck)方法就会返回0,这个时候就代表这个任务可以加入线程池并通过addWorker(null, false)来创建一个新的线程

else if (!addWorker(command, false)) 
    reject(command);

执行到了这里,这个时候就代表着线程池已经停止工作了。或者任务队列已经满了,这个时候线程池不能再加入更多的线程,通过addWorker来创建新的线程时,如果创建新的线程不成功,这个时候会执行reject来执行拒绝策略。

看到这里,可能大家还是会有点疑惑,这上面的代码并没有体现出线程复用的逻辑,其实玄机就在addWorker()这个方法,我们看下面的这张图
image.png
在addWorker()方法中,里面的核心代码其实是runWorker这个方法,其中入参的Worker就是我们提供给线程的任务队列,runWorker获取先获取Worker中最先进来的任务,然后通过一个while方法去循环判断,只要提供的线程任务不为null,那么她就会一直的循环执行。在这里,大家可能就会发现,我们提供给的多个线程任务,其实就是这一个while循环的线程中不断的去执行,所以每个线程始终都在一个大的循环中,反复取任务执行任务,从而达到了线程复用的作用。

总结

其实为什么线程池需要使用的线程复用,其原因就是线程池的优点,节省了线程创建和关闭的开销,以前我们普通执行线程,如果有1000个任务需要执行,这个时候就需要创建1000的线程,这个时候就会带来1000的线程的创建和销毁的开销。如果我们使用了线程池,通过线程池线程复用的原理,我们1000的线程任务,肯能都会在5个线程了循环执行,这样就节省了大量的线程创建销毁的开销,这就是使用线程复用的好处。