Java 动手写爬虫: 五 对象池
第五篇,对象池的设计与实现
前面每爬取一个任务都对应一个Job任务,试想一下,当我们爬取网页越来越多,速度越来越快时,就会出现频繁的Job对象的创建和销毁,因此本片将考虑如何实现对象的复用,减少频繁的gc
设计
我们的目标是设计一个对象池,用于创建Job任务,基本要求是满足下面几点:
- 可以配置对象池的容量大小
- 通过对象池获取对象时,遵循一下规则:
- 对象池中有对象时,总对象池中获取
- 对象池中没有可用对象时,新创建对象返回(也可以采用阻塞,直到有可用对象,我们这里采用直接创建新对象方式)
- 对象用完后扔回对象池
实现
1. 创建对象的工厂类 ObjectFactory
对象池在初始化对象时,借用对象工厂类来创建,实现解耦
public interface ObjectFactory<T> {
T create();
}
2. 对象池中的对象接口 IPollCell
因为每个对象都拥有自己的作用域,内部包含一些成员变量,如果对象重用时,这些成员变量的值,可能会造成影响,因此我们定义 IPoolCell
接口,其中声明一个方法,用于重置所有的变量信息
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 方法
- 首先是从队列中获取对象(非阻塞方式,获取不到时返回null而不是异常)
- 队列为空时,新建一个对象返回
- 未初始化队列,创建的对象表示可回收重复使用的
- 队列填满了,但是被其他线程获取完了,此时创建的对象理论上不需要重复使用,用完一次就丢掉
- release 方法
- 清空对象状态
- 扔进队列(非阻塞)
4. Job修改
既然要使用对象池,那么我们的IJob对象需要实现 IPoolCell接口了
将实现放在 DefaultAbstractCrawlJob
类中
@Override
public void clear() {
this.depth = 0;
this.crawlMeta = null;
this.fetchQueue = null;
this.crawlResult = null;
}
使用
上面只是实现了一个最简单的最基础的对象池,接下来就是适配我们的爬虫系统了
之前的创建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
相关博文
- Java 动手写爬虫: 一、实现一个最简单爬虫
- Java 动手写爬虫: 二、深度爬取
- Java 动手写爬虫: 三、爬取队列
- Java 动手写爬虫: 四、日志埋点输出 & 动态配置支持
- Java 动手写爬虫: 五 对象池
参考
个人博客:一灰的个人博客
- JSShell:一个基于python的交互式Shell
- golang中操作excel
- 企业安全漏洞通告引擎
- 通过httprouter和redis框架搭建restful api服务
- .NET Core 实现定时抓取博客园首页文章信息并发送到邮箱
- 基于 R 语言和 SPSS 的决策树算法介绍及应用
- Android 8.0 中的安全增强功能
- python2 群发 html 或文本邮件
- windows 安装 storm 及 eclipse 调试 TopN 实例
- shell 学习笔记(16)
- 根据ip查找ISP运营商和归属地的几种方法
- windows 安装 spark 及 pycharm 调试 TopN 实例
- storm kafka 编程指南
- 基于Session的身份窃取
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- 详解:小程序页面预加载优化,让你的小程序运行如飞
- RocketMQ学习六-消息存储
- swoole 实现 unixSocket 通信
- mybatis-plus一对多关联查询踩坑
- 深入Spring Security魔幻山谷-获取认证机制核心原理讲解
- 文本相似性的总结
- Java面试题总结之JDBC 和Hibernate
- Mac 下搭建 Clion + OpenCV4.x 的开发环境
- 超详细,Windows系统搭建Flink官方练习环境
- MySQL 覆盖索引与延迟关联
- Java面试题总结之数据结构、算法和计算机基础(刘小牛和丝音的爱情故事1)
- 在Java中什么时候才要考虑线程安全
- android功耗优化(2)--对齐唤醒
- Android 功耗(3)---高通功耗问题分析方法
- 搞定Java快速排序