【网络爬虫学习 --- 多线程技术提升爬虫性能】

 

多线程提升爬虫性能

 

为了提升爬虫性能,需要采用多线程的爬虫技术。并且开源软件Heritrix已经采用了多线程的爬虫技术来提高性能。而且很多大型网站都采用多个服务器镜像的方式提供同样的网页内容。采用多线程并行抓取能同时获取同一个网站的多个服务器中的网页,这样能极大地减少抓取这类网站的时间。

 

多线程是一种机制,它允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其他线程共享存储空间,这使得线程间的通信较进程简单。多个线程的执行是并发的,即在逻辑上是“同时”的。如果系统只有一个CPU,那么真正的“同时”是不可能的,但是由于CPU切换的速度非常快,用户感觉不到其中的区别,因此用户感觉到线程是同时执行的。

 

 

创建多线程的方法

 

在Java语言中,通过JDK提供的java.lang.Thread类或者java.lang.Runable接口,能够轻松地添加线程代码。

方法一:继承java.lang.Thread类,覆盖方法run(),在创建的java.lang.Thread 类的子类中重写run()方法。

这种方法简单明了,但是,它也有一个很大的缺陷,如果线程类MyThread已经从一个类继承(如小程序必须继承自Applet类),而无法再继承java.lang.Thread类时应该怎么办呢?这时,就必须使用下面的方法二来实现线程。

方法二:实现java.lang.Runnable接口

java.lang.Runnable接口只有一个run()方法,库创建一个类实现java.lang.Runnable接口并提供这一方法的实现,将线程代码写入run()方法中,并且新建一个java.lang.Thread类,将实现java.lang.Runnable的类作为参数传入,就完成了创建新线程的任务。

方法二使得能够在一个类中包容所有的代码,有利于封装。这种方法的缺点在于,只能使用一套代码,若想创建多个线程并使各个线程执行不同的代码,则必须额外创建类,如果这样的话,在大多数情况下也许还不如直接用多个类分别继承java.lang.Thread来得紧凑。

 

线程的状态以及状态间转换

 

java中,每个线程都需经历新生、就绪、运行、阻塞和死亡五种状态,线程从新生到死亡的状态变化称为生命周期。

用new运算符和Thread类或其子类建立一个线程对象后,该线程就处于新生状态。   

新生--->就绪:通过调用start()方法   

就绪--->运行:处于就绪状态的线程一旦得到CPU,就进入运行状态并自动调用自己的run()方法   

运行--->阻塞:处于运行状态的线程,执行sleep()方法,或等待I/O设备资源,让出CPU并暂时中止自己运行,进入阻塞状态   

阻塞--->就绪:睡眠时间已到,或等待的I/O设备空闲下来,线程便进入就绪状态,重新到就绪队列中等待CPU。当再次获得CPU时,便从原来中止位置开始继续运行。   

运行--->死亡: (1)(正常情况下)线程任务完成               

            (2)(非正常状况)线程被强制性的中止,如通过执行stop()或destroy()方法来终止一个线程

 

 

编写多线程程序通常会遇到线程的同步问题,及如何解决同步.

 

由于同一进程的多个线程共享存储空间,在带来方便的同时,也会带来访问冲突这个严重的问题,即并发访问时线程安全问题,线程安全问题的解决办法关键就是要保证容易出问题的代码的原子性。

所谓原子性是指:当A线程正在执行某段代码时,其他的线程则必须等到A线程执行完后,其他线程中才可以有一个线程再去执行A线程刚执行过的那段代码。

Java语言对于处理多线程同步的办法是利用了对象锁机制来实现对共享资源的互斥访问。

这主要是因为Java中任意类型的对象都由一个标志位(即对象锁)。该锁具有0和1两种状态,缺省状态为1。当某个线程执行了Synchronized(object)语句后,该object对象的标志位则改为0状态,直到执行完整个Synchronized语句中的代码后,该对象的标志位由系统自动改回1状态。所以当一个线程执行到Synchronized(object)语句时会先检查object对象的标志位,如果为0状态则表明已经有另外的线程正在执行Synchronized包括的代码,那么该线程将被暂时阻塞并放入等待队列尾部,直到其他的线程执行完对object的访问并将object对象的标志位置为1状态后,该线程的阻塞状态才被取消并进入运行态继续运行,该线程在得到object资源后系统将其标志位改为0状态以防止其他线程进入该同步代码块中。在创建启动线程之前,首先创建一个线程之间竞争使用的Object对象,然后将这个Object对象的引用传递给每一个线程对象。这样一来,每个线程都指向同一个Object对象锁所占据的资源。

Java语言中解决线程同步问题是依靠synchronized 关键字来实现的,它包括两种用法:synchronized 方法和synchronized 块。

(1) synchronized 方法。

通过在方法声明中加入synchronized关键字来声明该方法是同步方法,即多线程执行的时候各个线程之间必须顺序执行,不能同时访问该方法。如:

public  synchronized  void   accessVal(intnewVal);

在Java 中,不光是对象,每一个类也对应一把锁,因此也可将类的静态成员函数声明为synchronized,以控制其对类的静态成员变量的访问。

synchronized 方法的缺陷:若将一个执行时间较长的方法声明为synchronized,将会大大影响程序运行的效率。因此Java为我们提供了更好的解决办法,那就是synchronized 块。

(2) synchronized 块。

通过synchronized关键字来声明synchronized 块。语法如下:

synchronized(syncObject){

//允许访问控制的代码

}

synchronized 块是这样一种代码块,块的代码必须获得syncObject对象(如前所述,可以是类实例或类)的锁才能执行。由于synchronized块可以是任意代码块,且可任意指定上锁的对象,因此灵活性较高。

 

Java语言对线程阻塞的支持

 

阻塞指的是暂停一个线程的执行以等待某个条件发生(如等待资源就绪),学过操作系统的读者对它一定非常熟悉了。Java 提供了大量方法来支持阻塞,下面逐一分析。

(1) sleep()方法:

sleep()允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU时间片,指定的时间一过,线程重新进入可执行状态。例如,当线程等待某个资源就绪时,测试发现条件不满足后,让线程sleep()一段时间后重新测试,直到条件满足为止。

(2) suspend()和resume()方法:

两个方法配套使用,suspend()使线程进入阻塞状态,并且不会自动恢复,必须对其应用resume()方法,才能使得线程重新进入可执行状态。例如,当前线程等待另一个线程产生的结果时,如果发现结果还没有产生,会调用suspend()方法,另一个线程产生了结果后,调用resume() 使其恢复。

(3) yield()方法:

yield()方法使得线程放弃当前分得的CPU时间片,但不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得CPU时间。

(4) wait()和notify()方法:

两个方法配套使用,wait()可以使线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数。前者当对应的notify()被调用或者超出指定时间时,线程重新进入可执行状态;后者则必须在对应的notify()被调用时,线程才重新进入可执行状态。

 

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其他更多资源。线程对象也不例外。当前,比较流行的一种技术是“池化技术”,即在系统启动的时候一次性创业多个对象并且保存在一个“池”中,当需要使用的时候直接从“池”中取得而不是重新创建。这样可以大大提高系统性能。

Java语言在JDK1.5以后的版本中提供了一个轻量级线程池——ThreadPool。可以使用线程池来执行一组任务。简单的任务没有返回值,如果主线程需要获得子线程的返回值时,可以使任务实现Callable接口,线程池执行任务并通过Future的实例返回线程的执行结果。

Callable和java.lang.Runnable的区别如下:

Callable定义的方法是call(),而Runnable定义的方法是run()。

Callable的call()方法可以有返回值,而Runnable的run()方法不能有返回值。

Callable的call()方法可以抛出异常,而Runnable的run()方法不能抛出异常。

Future表示异步计算的结果,它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。

Future的cancel()方法取消任务的执行,cancel()方法有一个布尔参数,参数为true表示立即中断任务的执行,参数为false表示允许正在运行的任务运行完成。

Future的get()方法等待计算完成,获取计算结果。

 

下面的例子使用ThreadPool实现并行下载网页。在继承Callable方法的任务类中下载

网页的实现如下:

public class DownLoadCall implements    Callable<String>{
    private    URL    url; //待下载的URL
    public DownLoadCall(URL url){
        this.url=url;
    }
    @Override
    public String call() throws Exception{
        String content=null;
        //下载网页
        return content;
    }
}

主线程类创建ThreadPool并执行下载任务的实现如下:

        int    threads=4;//并发线程数量
        Executor Servicees = Executors.newFixedThreadPool(threads);//创建线程池
        Set<Future<String>> set = new HashSet<Future<String>> ();
        for(final URL url: urls){
            DownLoadCall task = new DownLoadCall(url);
            Future<String[]> future = es.submit(task);//提交下载任务
            set.add(future);
        }
        //通过future对象取得结果
        for(Future<String> future : set){
            String content = future.get();
        //处理下载网页的结果
        }

采用线程池可以充分利用多核CPU的计算能力,并且简化了多线程的实现。

 

多线程在爬虫中的应用

 

多线程爬虫的结构如图

对于并行爬虫架构而言,处理空队列要比序列爬虫更加复杂。空的队列并不意味着爬虫已经完成了工作,因为此刻其他的进程或者线程可能依然在解析网页,并且马上会加入新的URL。进程或者线程管理员需要给报告队列为空的进程/线程发送临时的休眠信号来解决这类问题。线程管理员需要不断跟踪休眠线程的数目;只有当所有的线程都休眠的时候,爬虫才可以终止。

 

资料为笔者学习笔记,非个人原创,版权归原作者所有.

转载请注明出处[http://www.cnblogs.com/dennisit/archive/2013/01/12/2857901.html]

  在线交谈

posted @ 2013-01-12 20:29  苏二  阅读(1913)  评论(0编辑  收藏  举报