Java 動手寫爬蟲: 五 對象池

第五篇,對象池的設計與實現

前面每爬取一個任務都對應一個Job任務,試想一下,當咱們爬取網頁愈來愈多,速度愈來愈快時,就會出現頻繁的Job對象的建立和銷燬,所以本片將考慮如何實現對象的複用,減小頻繁的gchtml

設計

咱們的目標是設計一個對象池,用於建立Job任務,基本要求是知足下面幾點:java

  • 能夠配置對象池的容量大小
  • 經過對象池獲取對象時,遵循一下規則:
    • 對象池中有對象時,總對象池中獲取
    • 對象池中沒有可用對象時,新建立對象返回(也能夠採用阻塞,直到有可用對象,咱們這裏採用直接建立新對象方式)
  • 對象用完後扔回對象池

實現

1. 建立對象的工廠類 ObjectFactory

對象池在初始化對象時,借用對象工廠類來建立,實現解耦git

public interface ObjectFactory<T> {

    T create();

}

2. 對象池中的對象接口 IPollCell

由於每一個對象都擁有本身的做用域,內部包含一些成員變量,若是對象重用時,這些成員變量的值,可能會形成影響,所以咱們定義 IPoolCell 接口,其中聲明一個方法,用於重置全部的變量信息github

public interface IPoolCell {

    /**
     * 清空全部狀態
     */
    void clear();

}

3. 一個簡單的對象池 SimplePool

package com.quick.hui.crawler.core.pool;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by yihui on 2017/8/6.
 */
@Slf4j
public class SimplePool<T extends IPoolCell> {

    private static SimplePool instance;


    public static void initInstance(SimplePool simplePool) {
        instance = simplePool;
    }

    public static  SimplePool getInstance() {
        return instance;
    }


    private int size;

    private BlockingQueue<T> queue;

    private String name;

    private ObjectFactory objectFactory;


    private AtomicInteger objCreateCount = new AtomicInteger(0);

    public SimplePool(int size, ObjectFactory objectFactory) {
        this(size, "default-pool", objectFactory);
    }

    public SimplePool(int size, String name, ObjectFactory objectFactory) {
        this.size = size;
        this.name = name;
        this.objectFactory = objectFactory;

        queue = new LinkedBlockingQueue<>(size);
    }


    /**
     * 獲取對象,若隊列中存在, 則直接返回;若不存在,則新建立一個返回
     * @return
     */
    public T get() {
        T obj = queue.poll();

        if (obj != null) {
            return obj;
        }

        obj = (T) objectFactory.create();
        int num = objCreateCount.addAndGet(1);


        if (log.isDebugEnabled()) {
            if (objCreateCount.get() >= size) {
                log.debug("objectPoll fulled! create a new object! total create num: {}, poll size: {}", num, queue.size());
            } else {
                // fixme 因爲併發問題,這個隊列的大小實際上與添加對象時的大小不必定相同
                log.debug("objectPoll not fulled!, init object, now poll size: {}", queue.size());
            }
        }

        return obj;
    }


    /**
     * 將對象扔回到隊列中
     *
     * @param obj
     */
    public void release(T obj) {
        obj.clear();

        // 非阻塞方式的扔進隊列
        boolean ans = queue.offer(obj);

        if (log.isDebugEnabled()) {
            log.debug("return obj to pool status: {}, now size: {}", ans, queue.size());
        }
    }


    public void clear() {
        queue.clear();
    }
}

上面的方法中,主要看get和release方法,簡單說明併發

  • get 方法app

    • 首先是從隊列中獲取對象(非阻塞方式,獲取不到時返回null而不是異常)
    • 隊列爲空時,新建一個對象返回
      • 未初始化隊列,建立的對象表示可回收重複使用的
      • 隊列填滿了,可是被其餘線程獲取完了,此時建立的對象理論上不須要重複使用,用完一次就丟掉
  • release 方法異步

    • 清空對象狀態
    • 扔進隊列(非阻塞)

4. Job修改

既然要使用對象池,那麼咱們的IJob對象須要實現 IPoolCell接口了ide

將實現放在 DefaultAbstractCrawlJob 類中測試

@Override
public void clear() {
    this.depth = 0;
    this.crawlMeta = null;
    this.fetchQueue = null;
    this.crawlResult = null;
}

使用

上面只是實現了一個最簡單的最基礎的對象池,接下來就是適配咱們的爬蟲系統了fetch

以前的建立Job任務是在 com.quick.hui.crawler.core.fetcher.Fetcher#start 中直接根據傳入的class對象來建立對象,所以,第一步就是着手改Fetcher類

1. 初始化對象池

建立方法修改,新增對象池對象初始化:Fetcher.java

public <T extends DefaultAbstractCrawlJob> Fetcher(Class<T> jobClz) {
    this(0, jobClz);
}


public <T extends DefaultAbstractCrawlJob> Fetcher(int maxDepth, Class<T> jobClz) {
    this(maxDepth, () -> {
        try {
            return jobClz.newInstance();
        } catch (Exception e) {
            log.error("create job error! e: {}", e);
            return null;
        }
    });
}

public <T extends DefaultAbstractCrawlJob> Fetcher(int maxDepth, ObjectFactory<T> jobFactory) {
    this.maxDepth = maxDepth;
    fetchQueue = FetchQueue.DEFAULT_INSTANCE;
    threadConf = ThreadConf.DEFAULT_CONF;
    initExecutor();

    SimplePool simplePool = new SimplePool<>(ConfigWrapper.getInstance().getConfig().getFetchQueueSize(), jobFactory);
    SimplePool.initInstance(simplePool);
}

說明

爲何將建立的對象池座位 SimplePool的靜態變量 ?

由於每一個任務都是異步執行,在任務執行完以後扔回隊列,這個過程不在 Fetcher對象中執行,爲了共享對象池,採用了這種猥瑣的方法

2. 啓動方法修改

在建立 Fetcher 對象時,已經初始化好對象池,所以start方法不須要接收參數,直接改成

public <T extends DefaultAbstractCrawlJob> void start() throws Exception {
  ....

  DefaultAbstractCrawlJob job = (DefaultAbstractCrawlJob) SimplePool.getInstance().get();
  job.setDepth(this.maxDepth);
  job.setCrawlMeta(crawlMeta);
  job.setFetchQueue(fetchQueue);

  executor.execute(job);
  
  ...

測試

測試代碼與以前有一點區別,即 Fetcher 在建立時選擇具體的Job對象類型,其餘的沒啥區別

public static class QueueCrawlerJob extends DefaultAbstractCrawlJob {

    public void beforeRun() {
        // 設置返回的網頁編碼
        super.setResponseCode("gbk");
    }

    @Override
    protected void visit(CrawlResult crawlResult) {
//            System.out.println(Thread.currentThread().getName() + "___" + crawlMeta.getCurrentDepth() + "___" + crawlResult.getUrl());
    }
}


@Test
public void testCrawel() throws Exception {
    Fetcher fetcher = new Fetcher(2, QueueCrawlerJob.class);

    String url = "http://chengyu.911cha.com/zishu_4.html";
    CrawlMeta crawlMeta = new CrawlMeta();
    crawlMeta.setUrl(url);
    crawlMeta.addPositiveRegex("http://chengyu.911cha.com/zishu_4_p[0-9]+\\.html$");

    fetcher.addFeed(crawlMeta);


    fetcher.start();
}

待改進

上面只是實現了一個最基本簡單的對象池,有很多能夠改進的地方

  • 對象池實例的維護,上面是採用靜態變量方式,侷限太強,致使這個對象池沒法多個共存
  • 對象池大小無法動態配置,初始化時設置好了以後就無法改
  • 可考慮新增阻塞方式的獲取對象

以上坑留待後續有空進行修改

3. 源碼地址

項目地址: https://github.com/liuyueyi/quick-crawler

對象池對應的tag: v0.008

相關博文

參考

我的博客:一灰的我的博客

公衆號獲取更多:

https://static.oschina.net/uploads/img/201707/09205944_0PzS.jpg

相關文章
相關標籤/搜索