本文属于java并发系列,转载原文是并发编程网的方腾飞大神所著,原文地址:http://ifeve.com/java-threadpool/
原文如下:
合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。
我们可以通过ThreadPoolExecutor来创建一个线程池。
1 |
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, |
2 |
keepAliveTime, milliseconds,runnableTaskQueue, threadFactory,handler); |
创建一个线程池需要输入几个参数:
RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。n AbortPolicy:直接抛出异常。
向线程池提交任务
我们可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务知否被线程池执行成功。通过以下代码可知execute方法输入的任务是一个Runnable类的实例。
01 |
threadsPool.execute( new Runnable() { |
02 |
@Override |
03 |
04 |
public void run() { |
05 |
06 |
// TODO Auto-generated method stub |
07 |
08 |
} |
09 |
10 |
}); |
我们也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。
01 |
try { |
02 |
03 |
Object s = future.get(); |
04 |
05 |
} catch (InterruptedException e) { |
06 |
07 |
// 处理中断异常 |
08 |
09 |
} catch (ExecutionException e) { |
10 |
11 |
// 处理无法执行任务异常 |
12 |
13 |
} finally { |
14 |
15 |
// 关闭线程池 |
16 |
17 |
executor.shutdown(); |
18 |
19 |
} |
我们可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,但是它们的实现原理不同,shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。
流程分析:线程池的主要工作流程如下图:
从上图我们可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:
线程池内部有一些状态,先来了解下这些状态的机制。以下用代码注释的方式来解释其中的含义
/* 这个是用一个int来表示workerCount和runState的,其中runState占int的高3位, 其它29位为workerCount的值。 workerCount:当前活动的线程数; runState:线程池的当前状态。 用AtomicInteger是因为其在并发下使用compareAndSet效率非常高; 当改变当前活动的线程数时只对低29位操作,如每次加一减一,workerCount的值变了, 但不会影响高3位的runState的值。当改变当前状态的时候,只对高3位操作,不会改变低29位的计数值。 这里有一个假设,就是当前活动的线程数不会超过29位能表示的值,即不会超过536870911, 就目前以及可预见的很长一段时间来讲,这个值是足够用了 */ private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); //COUNT_BITS,就是用来表示workerCount占用一个int的位数,其值为前面说的29 private static final int COUNT_BITS = Integer.SIZE - 3; /* CAPACITY为29位能表示的最大容量,即workerCount实际能用的最大值。 其值的二进制为:00011111111111111111111111111111(占29位,29个1) */ private static final int CAPACITY = (1 << COUNT_BITS) - 1; /* 以下常量是线程池的状态,状态存储在int的高3位,所以要左移29位。 腾出的低29位来表示workerCount 注意,这5个状态是有大小关系的。RUNNING来判断就可以了 */ /* RUNNING的含义:线程池能接受新任务,并且可以运行队列中的任务 -1的二进制为32个1,移位后为:11100000000000000000000000000000 */ private static final int RUNNING = -1 << COUNT_BITS; /* SHUTDOWN的含义:不再接受新任务,但仍可以执行队列中的任务 0的二进制为32个0,移位后还是全0 */ private static final int SHUTDOWN = 0 << COUNT_BITS; /* STOP的含义:不再接受新任务,不再执行队列中的任务,而且要中断正在处理的任务 1的二进制为前面31个0,最后一个1,移位后为:00100000000000000000000000000000 */ private static final int STOP = 1 << COUNT_BITS; /* TIDYING的含义:所有任务均已终止,workerCount的值为0, 转到TIDYING状态的线程即将要执行terminated()钩子方法. 2的二进制为00000000000000000000000000000010 移位后01000000000000000000000000000000 */ private static final int TIDYING = 2 << COUNT_BITS; /* TERMINATED的含义:terminated()方法执行结束. 3的二进制为00000000000000000000000000000011 移位后01100000000000000000000000000000 */ private static final int TERMINATED = 3 << COUNT_BITS;
各状态之间可能的转变有以下几种: RUNNING -> SHUTDOWN 调用了shutdown方法,线程池实现了finalize方法,在里面调用了shutdown方法,因此shutdown可能是在finalize中被隐式调用的 (RUNNING or SHUTDOWN) -> STOP 调用了shutdownNow方法 SHUTDOWN -> TIDYING 当队列和线程池均为空的时候 STOP -> TIDYING 当线程池为空的时候 TIDYING -> TERMINATED terminated()钩子方法调用完毕 /* 传入的参数为存储runState和workerCount的int值,这个方法用于取出runState的值。 ~为按位取反操作,~CAPACITY值为:11100000000000000000000000000000, 再同参数做&操作,就将低29位置0了,而高3位还是保持原先的值,也就是runState的值 */ private static int runStateOf(int c) { return c & ~CAPACITY; } /* 传入的参数为存储runState和workerCount的int值,这个方法用于取出workerCount的值。 因为CAPACITY值为:00011111111111111111111111111111,所以&操作将参数的高3位置0了, 保留参数的低29位,也就是workerCount的值。 */ private static int workerCountOf(int c) { return c & CAPACITY; } /* 将runState和workerCount存到同一个int中,这里的rs就是runState, 是已经移位过的值,填充返回值的高3位,wc填充返回值的低29位 */ private static int ctlOf(int rs, int wc) { return rs | wc; }好了,下面我们来看看线程池的核心流程。
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); }
逻辑如下:
1判断的参数command是否为null,为null就抛出NullPointerException。然后通过workerCountOf方法计算当前活动的线程数,如果当前活动的线程数小于corePoolSize,则增加一个线程addWorker。这里跟数据库线程池不太一样,也就是说当池中的线程数小于corePoolSize的时候,不管池中的线程是否有空闲的,每次调用该方法都去增加一个线程,直到池中的数目达到corePoolSize为止。
2.若当前线程数已经超过核心线程数且ctl状态等于RUNNING且工作成功进入工作等待队列,则我们进一步复查ctl。
先说else if 的情况:若ctl状态依旧为RUNNING,且调用workCountOf()方法检查发现此时的工作线程数为0时,将添加一个工作线程;
再说if:若此时ctl状态已经不为RUNNING,则尝试移除任务,并调用拒绝任务方法:reject(command)。
(有点绕,这里之所以需要复查ctl状态是由于在执行workQueue.offer(command)方法时,ctl状态随时可能由于调用shutDown方法或者shutDownNow方法而发生变化)
3.如果上述两种情况都不吻合,即此时已经有超过核心线程数的的线程在工作,且任务队列也已堆满,则尝试增加一个工作线程(如果此时线程数达到限定最大线程数,则会失败),若失败则调用拒绝任务方法reject(command).
private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { int c = ctl.get(); int rs = runStateOf(c); // Check if queue empty only if necessary. if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; for (;;) { int wc = workerCountOf(c); if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { final ReentrantLock mainLock = this.mainLock; w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { mainLock.lock(); try { // Recheck while holding lock. // Back out on ThreadFactory failure or if // shut down before lock acquired. int c = ctl.get(); int rs = runStateOf(c); if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); 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; }
代码有些长,大概逻辑如下:首先通过runStateOf方法取出存储在ctl中的状态值,在提示
// Check if queue empty only if necessary.
ctl状态为RUNNING状态或者为SHUTDOWN状态且此时任务队列仍有任务未执行完时,可以继续调用addWorker添加工作线程,但不能新建任务,即firstTask参数必须为null.否则这里将返回false,即新建工作线程失败。
接下来看 for (;;) ,这里面首先用workerCountOf方法取出当前活动的线程数,依据core参数,若core参数为true,即添加的为核心线程,则当前工作的线程数量不应当超过corePoolSize,否则返回false。若core参数为false,即添加的为普通线程,则当前工作的线程数量不应当超过maximumPoolSize,否则返回false。下来使用CAS操作将当前活动线程数加一,若加一成功,则跳出大循环,进入循环体后面的真正新增线程的地方;若加一不成功,判断下当前状态改变没有,若改变了则重新开始外层循环的下一次迭代,若状态没有改变,只是加一失败,那么就继续内层循环,直到加一成功。
接下来看如何新增线程。先获取了线程池的主锁mainLock。从而保证对共享资源workers的排他访问。这里通过new Worker(firstTask) 新建了一个worker对象,该对象持有他的第一个任务firstTask。在Worker的构造方法中,创建了一个线程对象,但这个线程是没有启动的。既然不能在构造方法里启动,那么就把创建的线程对象拿出来吧,也就是赋给了t变量。Woker类的构造方法,将通过ThreadFactory获取一个thread对象,这里可能失败,返回null,或者抛出异常。所以在上面的代码段中对这种情况作了处理,若返回null,则workerStarted将为false。
则继续往下recheck ctl状态(规则与1没有区别),通过校验则将当前worker放入workers数组中,然后重新校正队则池大小(largestPoolSize),置workerAdded标志位为true。最后通过wokerAdded标志位校验,置workerStarted标志位为true,启动线程,该线程持有当前worker对象,会执行worker对象的run方法,而woker对象的run方法又调用了自身的runWorker(this)方法。至此为止一个worker正式被添加进入workers数组并且正式开始运转。
最后finally判断是否启动,执行addWorkerFailed方法。
在继续其他方法之前,先说下Worker这个内部类。
private final class Worker extends AbstractQueuedSynchronizer implements RunnableWorker实现了Runnable接口,可以在后续作为Thread的构造方法参数用以创建线程。Worker还继承了AbstractQueuedSynchronizer类,只是简化每个Worker对象相关的锁的获取,在每次执行一个任务的时候,都需要持有这个锁。
/** 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 { 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); } }1.先说明一个变量completedAbruptly,这个布尔值被用于指代runWorker停止的原因,当completedAbruptly为true时代表worker停止是因为worker执行的外部业务逻辑代码抛出异常引起的。当completedAbruptly为false时代表worker停止是线程池内部工作机制下的正常退出。
2.获取当前执行线程,获取当前worker的第一个任务置于task变量中,之后执行w.firstTask = null。若不不将worker对象的firstTask置为空,则firstTask引用指向的task对象将不会被gc回收。将worker对象解锁,从而允许其在尚未获得task之前被中断。进入while循环,这里将调用getTask()方法获取任务task对象(可能造成线程等待),若因为各种原因(下面说明getTask()方法时会说到)返回null(即获取不到任务)则停止while循环,将completedAbruptly置为false。
3进入循环内部,代表已经获取到可执行的task对象,则锁定worker对象(保证不被shutDown方法中断),接着做条件判断,若ctl状态超过STOP态,或者当前线程已经因为线程池内部状态变化而被中断(如何判断中断是因为线程池内部状态变化而中断的?重复检查ctl状态即可,若线程的中断信号是在外部逻辑代码中设置的(不使用线程池的shutDownNow方法,直接使用的thread.interrupt()方法),则ctl状态不会为STOP,这样就可判定为不是因为线程池内部状态变化而引起的中断),则设置该工作线程为中断状态(wt.interrupt())。否则执行beforeExecute(wt,task)钩子方法,用户可以重写该方法而达到一些自定义需求(例如统计)。接着执行获得task的run方法,至此该worker成功获得并执行了一个任务。执行完了(正常执行结束或抛出异常)会调用afterExecute方法,afterExecute也是个钩子方法。执行期间抛出的异常都有处理,不再多说。最终将置task为空,增加worker的完成任务数(completedTasks),随后解锁worker。在最后有一个将completedAbruptly置为false的操作,如果线程能走到这里来,说明该线程在执行任务过程中没有抛出异常,也就是说该线程并不是异常结束的,而是正常结束的;如果走不到这一步,completedAbruptly的值还是初始值true,表示线程是异常结束的。
4线程结束时,会调用processWorkerExit方法做一些清理和数据同步的工作:该方法会处理处理统计信息,将worker的completedTask累加入线程池的completedTaskCount,并从线程池的workers中移除该worker,之后尝试终止线程池(因为这个worker可能是当前线程池中最后一个worker,tryTerminate方法应该在所有可能终止当前线程的地方被调用)。最后根据completedAbruptly的值选择策略,若为true,则直接调用addWorker增加一个新worker用以替代当前移除的worker;若为false则判断当前线程池大小是否小于允许的最小线程池大小(可能是corePoolSize也可能小于),若小于则调用addWorker增加一个新worker,否则什么也不做。
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(SHUTDOWN); interruptIdleWorkers(); onShutdown(); // hook for ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } tryTerminate(); }逻辑如下:
将获得主锁mainLock,并且查看是否有执行shutdown的权限,然后调用advanceRunState方法,将ctl状态置为shutdown。调用interruptIdleWorkers()方法关闭尚未获得task对象的worker(即还未执行到getTask()方法或者还未得到getTask()返回的worker)。之后调用onShutdown()钩子方法,用户可以在这里处理自定义逻辑。最后调用tryTerminate()方法尝试终止线程池。
final void tryTerminate() { for (;;) { int c = ctl.get(); if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) return; if (workerCountOf(c) != 0) { // Eligible to terminate interruptIdleWorkers(ONLY_ONE); return; } final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { try { terminated(); } finally { ctl.set(ctlOf(TERMINATED, 0)); termination.signalAll(); } return; } } finally { mainLock.unlock(); } // else retry on failed CAS } }tryTerminate()方法尝试终止线程池活动,满足终止条件的因素有两个:首先,ctl状态为STOP,或者为SHUTDOWN且任务队列为空,其次,ctl计数为0。如果状态是RUNNING,表示线程池还正在提供服务,不需要状态变换;如果状态为TIDYING或TERMINATED,池中的活动线程数已经是0,自然也不需要做什么操作了;若状态为SHUTDWON,但队列中还有任务,此时这些任务还需要做掉,因此池中的线程不能终止,因此,这种情况下也不需要做什么。
当状态为SHUTDOWN,且活动线程数为0的时候,就可以进入TIDYING状态了,进入TIDYING状态就可以执行钩子方法terminated(),该方法执行结束就进入了TERMINATED状态(参考前文中各状态的含义以及可能的状态转变)。最后的termination.signalAll()所为何事?当线程池shutdown后,外部可能还有很多线程在等待线程池真正结束,即调用了awaitTermination方法,该方法中,外部线程就是在termination上await的,所以,线程池关闭之前要唤醒这些等待的线程,告诉它们线程池关闭结束了。最后释放锁。
源码分析先到这里,真没想到,源码实现还是挺复杂的。
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如:
1 |
<b>protected</b> <b>void</b> beforeExecute(Thread t, Runnable r) { } |
http://my.oschina.net/zouqun/blog/407149