-
前言
用Java编写多线程程序已经是一个非常简单的事了,不过与其它多线程系统相比,一些高级特性在Java中仍然不具备,然而在J2SE5.0中这一切将会改变。J2SE5.0增加大量的线程相关类使得编写多线程程序更加容易! -
线程库-Thread Pools
线程库的基本思想简单的讲就是,一个线程库中拥有一定数量的线程,当有任务要执行时,就从线程库中找一个空闲的线程来执行这个任务,任务执行完后,该线程返回线程库等待下一个任务;如果线程库中没有空闲的线程来执行该任务,这时该任务将要等待直到有一个空闲的线程来执行它。这听起来有点不爽,那么我们为什么还要使用线程库呢? -
使用线程库的三大理由
-
重用线程能够获得性能上的好处。在多线程环境中,创建一个线程要花很高的代价。线程库使得线程能够被重用,当有大量任务要执行时,线程库避免了不断创建与销毁线程所带来的系统开销,使用得你的程序整体运行效率得到了一定程度的提高。
-
还有一个很重要的原因就是,线程库考虑到了较好的程序设计。如果有大量任务要执行,如果不用线程库,你不得不不重复创建一个线程、管理这个线程的生命同期的代码。断重复这些步骤是一件很乏味的工作,因为这与我们的业务逻辑(任务)无关。使用线程库,这一切将由线程库代为管理,你只需关注于你的商务逻辑,当要执行一个任务时,你只需简单的创建一个任务,并把它丢给线程库去执行就OK。
-
最后一个也是最为重要的一个:当有大量任务需要同时执行时,线程库能够带来重大的性能提升。这一点看上去与第一点相似,事实上,任何时候活动的线程都比CPU数量多得多,因此线程库能够充分利用CPU的时间片,使得程序看去上运行较快且高效。
-
-
如何使用线程库
在使用线程库需要做两件事:创建任务及建立线程库本身。
一个任务是一个实现了 Runnable或 Callable接口的对象。
线程库则基于 Executor接口。
java.util.concurrent.Executor
△
┆
java.util.concurrent.ExecutorService
△
│
java.util.concurrent.ThreadPoolExecutor
package java.util.concurrent;
public class ThreadPoolExecutor implements ExecutorService {
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
}
5. 两个重要概念Sizes与Queues
-
Size
一个线程库创建后,其的大小在最小值(corePoolSize)与最大值(maximumPoolSize)之间,运行时可根据getPoolSize()确定其当前的大小。 -
Queue
队列用于存放等候执行的任务。
下面一步步分析这两个值在线程库的工作中是如何运作:
1. ThreadPoolExecutor tpe = new ThreadPoolExecutor(M, N, TIMEOUT, TimeUnit.MILLISECONDS,
2. new LinkedBlockingQueue<Runnable>());
3.
4. Task[] tasks = new Task[nTasks];
5. for (int i = 0; i < nTasks; i++) {
6. tasks[i] = new Task(n, "Task " + i);
7. tpe.execute(tasks[i]);
8. }
9. tpe.shutdown();
-
行1-2构造了一个线程库,M个core threads和N maximum threads,这时实际上并没有线程被创建。(可能过prestartAllCoreThreads() 和 prestartCoreThread() 方法分别预先创建)
-
第7行 一个任务被加入到线程库,这时下列5种情况之一将会发生:
-
如果库中的线程数少于M,线程库将立即启动一个新的线程来运行这个任务。即使库中有空闲的线程,仍然会产生一个新线程直到数量到达M。
-
如果库中的线程数在M和N之间,并且只少有一个是空闲线程,那么任务将由这个空闲线程执行。
-
如果库中的线程数在M和N之间,并且没有空闲线程,这时库会检查存在的工作队列,如果任务能够放置在队列中而不被阻塞,那么任务就会放置在该列队中,不会有新的线程启动。
-
如果库中的线程数在M和N之间,并且没有空闲线程,且任务不能无阻塞地加入到队列中,这时库会开始一个新线程来运行这个任务。
-
如果库中的线程数量已到达N且没有空闲线程,这时库将会试着放置新任务到一个队列。如果该队列已到达它的最大大小,任务将会被拒绝,添加失败,否则任务将被接受等待有空闲的线程来运行它。
-
-
一个任务执行完成,运行这个任务的线程将去运行队列中的下一个任务,如果队列中没有任务,将会发生下面两种情况之一:
-
如果库中的线程数大于M,线程将等待一个队列中有新的任务。如果队列中的新任务没有超时,该线程将运行这个任务,否则线程将退出以减少库中的线程数量。超时值是在新构造线程库时指定的TIMEOUT,如果TIMEOUT为0,不管库的最小大小(corePoolSize)是多少,线程执行完任务后总是退出。
-
如果库中的线程数量等于M或小于M,该线程将不确定地被阻塞以等待一个新的任务被加入到队列(除非TIMEOUT=0,它将退出),当它有效时就会运行这个新任务。
-
-
3. 队列的选择
上述的一切意味着什么?意味着选择库的大小及队列将对性能的提高是很重要的。通常有三种选择:
-
SynchronousQueue,它相当于一个0长度的队列。无论什么时候,加入一个任务到队列都会失败。这意味着一个任务不是立即被执行就是被拒绝。当然你可以指定一个无限大小的最大线程数量来防止任务被拒绝,不过这样就失去了使用线程所带来的好处。
-
无边界队列,如一个不限大小的 LinkedBlockingQueue,这种情况添加一个任务到队列总会成功,这同时说明线程库永远不会创建多于M的线程且任务永远不会被拒绝。
-
有边界队列,如一个有固定大小的 LinkedBlockingQueue或一个 ArrayBlockingQueue。假设这个边界值为P。当任务将被加入到库中时,库将创建线程直到线程数达到M;在这以后,加入的任务将放入队列排队直到队列中的任务数量达到P;当还有更多的任务要加入时,库将增加新的线程直到其数量达到N。如果库中的线程数量达到了N且队列中的任务数也达到了P,这时加入的任务将会被拒绝。