网络请求通常有两种形式:第一种,请求不是很频繁,而且每次连接后会保持相当一段时间来读数据或者写数据,最后断开,如文件下载,网络流媒体等。另一种形式是请求频繁,但是连接上以后读/写很少量的数据就断开连接。考虑到服务的并发问题,如果每个请求来到以后服务都为它启动一个线程,那么这对服务的资源可能会造成很大的浪费,特别是第二种情况。因为通常情况下,创建线程是需要一定的耗时的,设这个时间为T1,而连接后读/写服务的时间为T2,当T1>>T2时,我们就应当考虑一种策略或者机制来控制,使得服务对于第二种请求方式也能在较低的功耗下完成。
通常,我们可以用线程池来解决这个问题,首先,在服务启动的时候,我们可以启动好几个线程,并用一个容器(如线程池)来管理这些线程。当请求到来时,可以从池中去一个线程出来,执行任务(通常是对请求的响应),当任务结束后,再将这个线程放入池中备用;如果请求到来而池中没有空闲的线程,该请求需要排队等候。最后,当服务关闭时销毁该池即可。
线程池中通常由这样几个概念(接口)组成:
整个池的机制和结构就是这样,当然,需要一个调度者(scheduler)来协调主线程和池的关系。结构,或者接口的目的是为了让我们从细节中解脱出来,从一个比较抽象的层次来描述系统,这样的好处是简单,而且设计出来的框架比较通用,可以适应很多相近相似的情况。由于Task具体干什么我们不知道,所以它几乎可以干任何适应于上边总结的网络连接的第二种情况(T1>>T2)。
虽然为一个简单的实现设计一个标准的UML视图是不太现实的,但是这是一种受鼓励的做法,至少应该用铅笔在草纸上画出相关的视图,这样可以帮助以后的维护和更高级的扩展。
实现可以是通过多种语言的,我们在此选择面向对象的JAVA,而如果你使用C的话,也没有问题,问题在上一小节已经描述清楚,语言是不重要的。
池是一个容器,我们考虑使用java.util.LinkedList类(可能由于它的长度是可变的,而且不需要我们使用者来考虑),也就是说,池需要维护一个链表。
public interface Pool {//池接口
Executor getExecutor();
void destroy();
}
public interface Executor {//执行器接口
void setTask(Task task);
Task getTask();
void startTask();
}
鉴于执行器是池中的对象,而且外部没有必要知道其细节,我们考虑将Executor接口的实现做为Pool接口的实现的内部类。这样做的另一个好处是,更便于池的管理。
import java.util.LinkedList;
import java.util.Properties;
import redesigned.utils.PropReader;
public class ThreadPool implements Pool{
private boolean isShut;
private LinkedList pool;
private static Properties prop = PropReader.getProperties("webconfig.properties");
private int size = Integer.parseInt(prop.getProperty("threadsperpage", "3"));
public ThreadPool(){
// read configuration and set the
// content of pool by objects of Executor
isShut = false;//set the status of pool to active
pool = new LinkedList();
for(int i = 0; i < size; i++){
Executor executor = new ExecutorImpl();//new a executor thread
pool.add(executor);//add it to pool
((ExecutorImpl)executor).start();//start it
}
}
public void destroy() {//Destroy
synchronized(pool){
isShut = true;//set the status of pool to inactive
pool.notifyAll();//notify all listener.
pool.clear();//clear the list of threads
}
}
public Executor getExecutor(){
Executor ret = null;
synchronized(pool){//return if any.
if(pool.size() > 0){
ret = (Executor)pool.removeFirst();
}else{
try {
pool.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
ret = (Executor)pool.removeFirst();
}
}
return ret;
}
Executor接口的实现作为ThreadPool的内部类
private class ExecutorImpl extends Thread implements Executor{
private Task task;
private Object lock = new Object();
//private boolean loop = true;
public ExecutorImpl(){}
public Task getTask() {
return this.task;
}
public void setTask(Task task) {
this.task = task;
}
public void startTask(){
//System.out.println("start here");
synchronized(lock){
lock.notify();
}
}
public void run(){
//get a task if any
//then run it
//then put self to pool
while(!isShut){
synchronized(lock){
try {
lock.wait();//wait for resource
} catch (InterruptedException e) {
e.printStackTrace();
}
}
getTask().execute();//execute the task
synchronized(pool){//put it self to the pool when finish the task
pool.addFirst(ExecutorImpl.this);
pool.notifyAll();
}
}
}
}
}
好了,池设计好了,再来看看任务(Task)的接口和实现
public interface Task {//这个接口也比较简单,可以执行,可以取到执行结果
void execute();
byte[] getResult();
}
Task的实现可以是多种多样的,下边的例子是一个加载资源的Task.使用方式
Pool pool = new ThreadPool();// new a ThreadPool
//load resources on each page, and start #s of thread.
for(int i = 0; i < resourceList.size();i++){
Executor executor = pool.getExecutor(); // get Executor form pool
Task resourceLoader = new ResourceLoader((String)resourceList.get(i));
executor.setTask(resourceLoader); // set the task to executor
executor.startTask(); // try to start the executor.
}
//wait while all task are done, the destroy the pool.
pool.destroy();
同步问题: 同步在线程的并发中意义非常之大,对临界资源的控制是并发时最关键的地方。如在线程池中,当池中没有空闲的线程时,新来的请求就必须等待,而一旦一个Task运行结束后,一方面将自己放入池中,一方面需要通知等待在pool中的其他线程。每一个执行器线程,一开始启动,则进入等待状态,此时不会消耗CPU资源。而当在外部调用执行器的startTask()方法,即可通知线程从等待状态中醒来,去出Task,执行之,将执行器本身放入池中,然后继续等待。
当然,实现的策略是可以多种多样的,但是问题的本质已经在第二小节结构 很明确的被定义了。
最近开始学习JAVA,同时也开始熟悉面向对象的思维方式,这篇日志,一来作为一个备忘,二来可以对可能需要的人提供帮助。以上可以算作是小结。