3.6.2 创建线程池
在3.6.1节介绍的实现方式中,对每个客户都分配一个新的工作线程。当工作线程与客户通信结束,这个线程就被销毁。这种实现方式有以下不足之处。
服务器创建和销毁工作线程的开销(包括所花费的时间和系统资源)很大。如果服务器需要与许多客户通信,并且与每个客户的通信时间都很短,那么有可能服务器为客户创建新线程的开销比实际与客户通信的开销还要大。
除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。每个线程本身都会占用一定的内存(每个线程需要大约1M内存),如果同时有大量客户连接服务器,就必须创建大量工作线程,它们消耗了大量内存,可能会导致系统的内存空间不足。
如果线程数目固定,并且每个线程都有很长的生命周期,那么线程切换也是相对固定的。不同操作系统有不同的切换周期,一般在20毫秒左右。这里所说的线程切换是指在Java虚拟机,以及底层操作系统的调度下,线程之间转让CPU的使用权。如果频繁创建和销毁线程,那么将导致频繁地切换线程,因为一个线程被销毁后,必然要把CPU转让给另一个已经就绪的线程,使该线程获得运行机会。在这种情况下,线程之间的切换不再遵循系统的固定切换周期,切换线程的开销甚至比创建及销毁线程的开销还大。
线程池为线程生命周期开销问题和系统资源不足问题提供了解决方案。线程池中预先创建了一些工作线程,它们不断从工作队列中取出任务,然后执行该任务。当工作线程执行完一个任务时,就会继续执行工作队列中的下一个任务。线程池具有以下优点:
减少了创建和销毁线程的次数,每个工作线程都可以一直被重用,能执行多个任务。
可以根据系统的承载能力,方便地调整线程池中线程的数目,防止因为消耗过量系统资源而导致系统崩溃。
如例程3-6所示,ThreadPool类提供了线程池的一种实现方案。
例程3-6 ThreadPool.java
package multithread2; import java.util.LinkedList; public class ThreadPool extends ThreadGroup { private boolean isClosed=false; //线程池是否关闭 private LinkedList<Runnable> workQueue; //表示工作队列 private static int threadPoolID; //表示线程池ID private int threadID; //表示工作线程ID public ThreadPool(int poolSize) { //poolSize指定线程池中的工作线程数目 super("ThreadPool-" + (threadPoolID++)); setDaemon(true); workQueue = new LinkedList<Runnable>(); //创建工作队列 for (int i=0; i<poolSize; i++) new WorkThread().start(); //创建并启动工作线程 }
/** 向工作队列中加入一个新任务,由工作线程去执行该任务 */ public synchronized void execute(Runnable task) { if (isClosed) { //线程池被关则抛出IllegalStateException异常 throw new IllegalStateException(); } if (task != null) { workQueue.add(task); notify(); //唤醒正在getTask()方法中等待任务的工作线程 } } /** 从工作队列中取出一个任务,工作线程会调用此方法 */ protected synchronized Runnable getTask()throws InterruptedException{ while (workQueue.size() == 0) { if (isClosed) return null; wait(); //如果工作队列中没有任务,就等待任务 } return workQueue.removeFirst(); } /** 关闭线程池 */ public synchronized void close() { if (!isClosed) { isClosed = true; workQueue.clear(); //清空工作队列 interrupt(); //中断所有的工作线程,该方法继承自ThreadGroup类 } } /** 等待工作线程把所有任务执行完 */ public void join() { synchronized (this) { isClosed = true; notifyAll(); //唤醒还在getTask()方法中等待任务的工作线程 } Thread[] threads = new Thread[activeCount()]; //enumerate()方法继承自ThreadGroup类,获得线程组中当前所有活着的工作线程 int count = enumerate(threads); for (int i=0; i<count; i++) { //等待所有工作线程运行结束 try { threads[i].join(); //等待工作线程运行结束 }catch(InterruptedException ex) { } } } /** 内部类:工作线程 */ private class WorkThread extends Thread { public WorkThread() { //加入到当前ThreadPool线程组中 super(ThreadPool.this,"WorkThread-" + (threadID++)); } public void run() { while (!isInterrupted()) { //isInterrupted()方法继承自Thread类,判断线程是否被中断 Runnable task = null; try { //取出任务 task = getTask(); }catch (InterruptedException ex){} // 如果getTask()返回null或者线程执行getTask()时被中断,则结束此线程 if (task == null) return;
try { //运行任务,异常在catch代码块中捕获 task.run(); } catch (Throwable t) { t.printStackTrace(); } } //#while } //#run() } //#WorkThread类 } |
在ThreadPool类中定义了一个LinkedList类型的workQueue成员变量,它表示工作队列,用来存放线程池要执行的任务,每个任务都是Runnable实例。ThreadPool类的客户程序(利用ThreadPool来执行任务的程序)只要调用ThreadPool类的execute (Runnable task)方法,就能向线程池提交任务。在ThreadPool类的execute()方法中,先判断线程池是否已经关闭。如果线程池已经关闭,就不再接收任务,否则就把任务加入到工作队列中,并且唤醒正在等待任务的工作线程。
在ThreadPool类的构造方法中,会创建并启动若干工作线程,工作线程的数目由构造方法的参数poolSize决定。WorkThread类表示工作线程,它是ThreadPool类的内部类。工作线程从工作队列中取出一个任务,接着执行该任务,然后再从工作队列中取出下一个任务并执行它,如此反复。
工作线程从工作队列中取任务的操作是由ThreadPool类的getTask()方法实现的,它的处理逻辑如下:
如果队列为空并且线程池已关闭,那就返回null,表示已经没有任务可以执行了;
如果队列为空并且线程池没有关闭,那就在此等待,直到其他线程将其唤醒或者中断;
如果队列中有任务,就取出第一个任务并将其返回。
线程池的join()和close()方法都可用来关闭线程池。join()方法确保在关闭线程池之前,工作线程把队列中的所有任务都执行完。而close()方法则立即清空队列,并且中断所有的工作线程。
ThreadPool类是ThreadGroup类的子类。ThreadGroup类表示线程组,它提供了一些管理线程组中线程的方法。例如,interrupt()方法相当于调用线程组中所有活着的线程的interrupt()方法。线程池中的所有工作线程都加入到当前ThreadPool对象表示的线程组中。ThreadPool类在close()方法中调用了interrupt()方法:
/** 关闭线程池 */ public synchronized void close() { if (!isClosed) { isClosed = true; workQueue.clear(); //清空工作队列 interrupt(); //中断所有的工作线程,该方法继承自ThreadGroup类 } } |
以上interrupt()方法用于中断所有的工作线程。interrupt()方法会对工作线程造成以下影响:
如果此时一个工作线程正在ThreadPool的getTask()方法中因为执行wait()方法而阻塞,则会抛出InterruptedException;
如果此时一个工作线程正在执行一个任务,并且这个任务不会被阻塞,那么这个工作线程会正常执行完任务,但是在执行下一轮while (!isInterrupted()) {…}循环时,由于isInterrupted()方法返回true,因此退出while循环。
如例程3-7所示,ThreadPoolTester类用于测试ThreadPool的用法。
例程3-7 ThreadPoolTester.java
package multithread2; public class ThreadPoolTester { public static void main(String[] args) { if (args.length != 2) { System.out.println( "用法: java ThreadPoolTest numTasks poolSize"); System.out.println( " numTasks - integer: 任务的数目"); System.out.println( " numThreads - integer: 线程池中的线程数目"); return; } int numTasks = Integer.parseInt(args[0]); int poolSize = Integer.parseInt(args[1]); ThreadPool threadPool = new ThreadPool(poolSize); //创建线程池 // 运行任务 for (int i=0; i<numTasks; i++) threadPool.execute(createTask(i));
threadPool.join(); //等待工作线程完成所有的任务 // threadPool.close(); //关闭线程池 }//#main()
/** 定义了一个简单的任务(打印ID) */ private static Runnable createTask(final int taskID) { return new Runnable() { public void run() { System.out.println("Task " + taskID + ": start"); try { Thread.sleep(500); //增加执行一个任务的时间 } catch (InterruptedException ex) { } System.out.println("Task " + taskID + ": end"); } }; } } |
ThreadPoolTester类的createTask()方法负责创建一个简单的任务。ThreadPoolTester类的main()方法读取用户从命令行输入的两个参数,它们分别表示任务的数目和工作线程的数目。main()方法接着创建线程池和任务,并且由线程池来执行这些任务,最后调用线程池的join()方法,等待线程池把所有的任务执行完毕。
运行命令“java multithread2.ThreadPoolTester 5 3”,线程池将创建3个工作线程,由它们执行5个任务。程序的打印结果如下:
Task 0: start Task 1: start Task 2: start Task 0: end Task 3: start Task 1: end Task 4: start Task 2: end Task 3: end Task 4: end |
从打印结果看出,主线程等到工作线程执行完所有任务后,才结束程序。如果把main()方法中的“threadPool.join()”改为“threadPool.close()”,再运行程序,则会看到,尽管有一些任务还没有执行,程序就运行结束了。
如例程3-8所示,EchoServer利用线程池ThreadPool来完成与客户的通信任务。
例程3-8 EchoServer.java(使用线程池ThreadPool类)
package multithread2; import java.io.*; import java.net.*; public class EchoServer { private int port=8000; private ServerSocket serverSocket; private ThreadPool threadPool; //线程池 private final int POOL_SIZE=4; //单个CPU时线程池中工作线程的数目 public EchoServer() throws IOException { serverSocket = new ServerSocket(port); //创建线程池 //Runtime的availableProcessors()方法返回当前系统的CPU的数目 //系统的CPU越多,线程池中工作线程的数目也越多 threadPool= new ThreadPool( Runtime.getRuntime().availableProcessors() * POOL_SIZE); System.out.println("服务器启动"); } public void service() { while (true) { Socket socket=null; try { socket = serverSocket.accept(); threadPool.execute(new Handler(socket)); //把与客户通信的任务交给线程池 }catch (IOException e) { e.printStackTrace(); } } } public static void main(String args[])throws IOException { new EchoServer().service(); } }/** 负责与单个客户通信的任务,代码与3.6.1节的例程3-5的Handler类相同 */ class Handler implements Runnable{…} |
在以上EchoServer的service()方法中,每接收到一个客户连接,就向线程池ThreadPool提交一个与客户通信的任务。ThreadPool把任务加入到工作队列中,工作线程会在适当的时候从队列中取出这个任务并执行它。
【责任编辑:
雪花 TEL:(010)68476606-8007】