转自:《计算机工程》 文/ 董瑞洪,张秋余,唐静兵,张涛
线程
线程是描述进程内的执行,正是线程负责执行包含在进程的地址空间中的代码。单个进程可能包含几个线程,它们可以同时执行进程的地址空间中的代码。每个线程有自己的一组cpu寄存器和堆。线程可以看成“一段代码的执行”也就是一系列有jvm执行的二进制指令。这里面没有对象甚至没有方法的概念。线程是有序的指令,而不是方法。
线程的数据结构,仅仅只包括执行这些指令的信息。它包含当前的运行上下文,如寄存器的内容,当前指令的在运行引擎的指令流中的位置,保存方法本地参数和变量的运行时堆栈。切换线程更有效率,时间单位是us。对于java而言,一个线程可以看作是jvm的一个状态。
创建线程
java使用两种方法来处理多线程。
1.子类化Thread对象。Thread对象是java提供的封装线程的对象。要创建一个线程,必须创建一个从Thread对象类导出的新类。必须覆盖 Thread的run()函数来完成有用的工作。如果使用的类需要子类化另一个类,就不能子类化Thread对象。
2.实现Runnable接口。Runnable接口只有一个方法run(),我们声明自己的类实现Runnable接口并提供这一方法,将线程代码写入其中,就完成了这一部分的任务。但是Runnable接口并没有任何对线程的支持,还必须创建Thread类的实例,这一点通过Thread类的构造函数 public Thread(Runnable target)来实现。
线程间通信
当线程在继续执行当前需要等待一个条件时,仅有synchronized关键字是不够的。虽然synchronized关键字阻止并非更新一个对象,但它并没实现线程间通信。Object类为此提供了3个函数:wait(),notify()和notifyAll()。以全球气候预测程序为例。这些程序通过将地球分为许多单元,在每个循环中,每个单元的计算都是隔离进行的,知道这些值趋于稳定,然后相邻单元之间就会交换一些数据。所以,从本质上讲,在每个循环中各个线程都必须等待所有线程完成各自的任务以后才能进入下一个循环。模型称为屏蔽同步。
spider程序的结构
构造spider程序有两种方式:1.把能spider程序设计为递归的程序 2.编写一个非递归的程序,它要维护一个要访问的网页列表。考虑使用哪一种方式的前提是,构造的spider程序必须能访问非常大的web站点。
本系统中使用了非递归的程序设计方法。这是因为,当一个递归程序运行时要把每次递归压入堆栈,但在本系统设计中使用的是多线程,它允许一次运行多个任务,但是,多线程与递归是不兼容的。因为在这一过程中每个线程都有自己的堆栈,而当一个方法调用它自身时,他们需要使用同一个堆栈。这意味着递归的 spider程序不可能使用多线程。
数据库设计
在程序执行的过程中,考虑对储存网页的储存过程进行监控,以便决定spider程序对访问队列处理。因为,在数据库设计过程中,添加了网页储存状态这个字段。
在Internet中,有很大一部分网页数据大于8000byte,如果用普通的二进制型字段(binary,varbinary)是不能完成存储要求的。因而,设计数据库中储存网页数据的字段为BLOB字段,实际使用为text类型。这就要求对该字段的储存和读取过程中,不能按照常规的字段读取方式进行。
一个URL对应一个网页,选取网页数据对应的URL对位数据库中记录的主键,进行唯一标识。
类的设计
Getsite类,spider类,spiderInternalWolkload类,spiderWorker类,spiderDone 类,spiderSqlWorkload类,接口 spiderReportable, IWorkloadStorable
GetSite类为程序的入口,负责整个spider程序的运行。
当创建一个spider程序时,需要使用spider类。spider类服务于3个目的:
1.担当spider接口,为使用spider提供方法;
2.这个对象管理线程池,并将spider的发现向启动此spider的对象报告(即spider对象和spiderWorker对象间通信)
3.检测spider何时完成是spider的任务。spider类必须坚持spider何时完成。这一任务有spiderdone类来完成。
SpiderWorker对象的目的是处理URL。
由于有很多并发的线程,要确切地知道spider何时完成是相当困难的。这需要一个对象来精确地跟踪有多少个线程仍在运行,spiderDone类就是为完成这一任务来设计的。
抢先式多线程spider程序的实现
由于spider是多线程的,必须有一种方法在不同的线程间分配任务。这个工作死有spiderWorker对象来完成的。通过这个对象维护线程池,可以承担spider创建和销毁线程对象的任务。
我们的网络爬虫广泛使用了多线程,必须把任务分成很多小任务。spider的任务是下载一个web站点,并将网页内的链接加入到作业中来。 spiderWorker实现了这一个基本任务。当spider开始时,它创建一个处理spider发现网页的spiderWorker类池。
线程创建
用spider的start()函数,该函数再调用run()。SpiderWorker对象的生成在spider类中,如以下代码所示:
public Spider(ISpiderReportable manager,String URL,HTTP http,int poolSize,IWorkerloadStorable w) {
_manager = manager;
_workSpider = false;
_pool = new SpiderWorker[poolSize];
for( int i = 0; i< _pool.length;i++){
HTTP hc = http.copy();
_pool[i] = new SpiderWorker( this,hc);
}
}
其中,最后一行代码生成SpiderWorker对象。也就是说,在构造Spider对象的同时,生成了SpiderWorker对象进行线程的管理,并且生成了线程池。SpiderWorker对象的start()方法被调用后,开始执行SpiderWorker的run()方法,为进一步处理网页做好了准备。
线程调用
在已经初始化线程,发现待处理URL队列的基础上,SpiderWorker对象调用SpiderDone类的workerBegin()方法,唤醒已经初始化过的spider线程。
synchronize public void workBegin() {
_activeThreads ++;
_started = true;
notify();
}
线程的唤醒调用notify()方法。
线程关闭
紧接着调用SpiderWorker对象的processWorkload()方法,进入到网页处理的真正执行代码,转入到类GetSite中。 processWorkload方法执行完毕,也就是说,该线程完成了自己的历史使命,执行了网页处理的过程,就应该为其他的线程让出资源,因为就应关闭该线程。执行SpiderDone类的 workerEnd()方法,如以下代码所示:
synchronized public void workerEnd() {
_activeThreads -- ;
notify();
}