关于线程池的几个问题
这里以java中的 ThreadPoolExecutor 线程池来说明:
1:线程池的几个重要的参数,这几个参数有什么作用?
在线程中有 核心池大小,最大池大小,任务队列 这几个比较重要的参数。
以下是几个重要参数在源码中的体现
// 核心池大小 private volatile int corePoolSize; // 最大池大小 private volatile int maximumPoolSize; // 任务队列 private final BlockingQueue<Runnable> workQueue;
作用:
corePoolSize:当需要执行的任务数量小于corePoolSize则新创建一个线程,并且执行任务,如果执行任务数大于corePoolSize则将任务,则将需要执行的任务放入到workQueue队列中,以下是代码:
// 小于核心池大小,则新增一个线程然后执行 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); }
maximumPoolSize:当队列中的任务已经添加满了,再往池中添加任务,如果 池中线程数<maximumPoolSize则会新建线程,反之则废弃该任务
// 添加到队列中失败,则会新建线程去执行任务 else if (!addWorker(command, false)) reject(command);
workQueue:当池中的线程数大于等于核心池的大小,则将需要执行的任务放入到队列workQueue中。
2:线程池的工作流程是什么?
分为3中场景的流程:1:池中线程数<核心池数 2:池中线程数大于等于核心池数但小于最大池数 3:池中线程=最大池数
场景1:新建一个线程并且执行任务,进入到 addWorker 的方法中:
// 添加一个任务到workSet中,core 为true private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { int c = ctl.get(); // 获取线程池的运行状态 int rs = runStateOf(c); …… for (;;) { // 线程池中运行的线程数 int wc = workerCountOf(c); if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; // 原子增加 执行线程的数量 if (compareAndIncrementWorkerCount(c)) // 跳出循环 进入循环体外的逻辑 break retry; …… } } 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 { …… int rs = runStateOf(ctl.get()); if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); // 将任务放到 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 { if (! workerStarted) addWorkerFailed(w); } return workerStarted; }
下面看看具体的执行方法 即 t.start(); 方法的具体逻辑,代码如下:
//执行 start方法 会运行run 方法 public void run() { runWorker(this); }
下面是 runWorker 的方法:task != null表示需要执行的任务不为空,在池中线程<核心池大小为true getTask():从队列中获取任务并且执行。
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; try { // 进入while循环,使该线程可以一直处理任务,线程不会执行完一次任务就退出,在线程池退出前,线程一直工作,以免线程资源的浪费 while (task != null || (task = getTask()) != null) { w.lock(); try { beforeExecute(wt, task); Throwable thrown = null; try { // 具体执行任务 task.run(); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }
当这个线程执行完任务之后,并不会销毁线程,而是不断遍历队列中的任务,如果队列中有任务,则执行相应的任务。这个是线程池的主要实现逻辑,即达到了线程复用的目的。
场景2:池中线程数大于等于核心池数但小于最大池数
// 这里将任务放入到队列中 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); }
放入到队列中,具体的执行时刻是在,场景1中的 while循环中的 getTask() 获取到任务,然后执行
场景3:
// 将任务加到workerSet中,并且新建一个线程去执行,这里的core标识为false, 其余逻辑和场景1中是一致的 else if (!addWorker(command, false)) reject(command);
3:如何设置核心池的大小?如何计算?
如果是计算密集型,这时是不需要切换线程来计算的,开启的线程数为 CPU个数
如果是IO阻塞密集,则根据实际的阻塞时间来进行计算,如:执行计算时间 为 a 阻塞时间为 b 这时线程池的大小应该设置为 (b/a+1)*cpu 核数 这个只是理论指导值