轻量级线程池的实现

这里写图片描述

写在前面

最近因为项目需要,自己写了个单生产者-多消费者的消息队列模型。多线程真的不是等闲之辈能玩儿的,我花了两个小时进行设计与编码,却花了两天的时间调试与运行。在这里,我把我遇到的坑与大家分享。

需求的由来

一开始我需要实现一个记录用户操作日志的功能,目的是给商家用户提供客户行为分析的能力。要记录的信息包括客户的访问时间、IP、在网站上所做的操作等。其中,客户的地域信息是个重要的分析项,所以必须要把IP转化成省市县。那么究竟何时完成这个转化的动作呢?有两种方案:
1. 在用户进行数据分析完成转化
2. 在用户进行数据分析完成转化
第一种方案显然不靠谱,因为需要转化的IP数量很大,而且转化采用第三方接口,因此整个转化过程将持续很长很长很长……的时间。

而在分析前就把转化过程完成掉,这样当用户需要分析的时候就可以减少这部分时间的开销,提高了响应速度。因此第二种方案显然比较合理。

那么随之而来的问题是:究竟在数据分析前的哪个时机进行转化?
这个问题又有两种方案:
1. 在记录日志的时候就立即完成IP向省市县的转换;
2. 每天半夜服务器统一把当天的IP转化成省市县;
这两种方案应该来说各有千秋。
第一种方案比较消耗服务器资源,因为IP向省市县转化需要向第三方接口发送GET请求,因此需要消耗一定的出口带宽和内存资源,在服务器资源一定的前提下,分给用户访问的资源就会被减少,从而可能会影响请求响应速度。但这个问题可以用钱来解决,只要花钱砸服务器就行了;而第二种方案在服务器空闲的时候进行转化虽然节约了服务器资源,但这也导致了商家的分析结果会有一天的滞后,影响用户体验。

于是,这个问题就变成了老板的钱重要还是用户体验重要。因此我毫不犹豫地选择了第一种方案。

初步设计

我使用Servlet Filter拦截用户的所有请求,并在Filter中获取用户的各项信息(其中包括IP),然后再请求第三方接口,完成IP向省市县的转化,最后将这些信息入库。

这个流程很显然有重大缺陷:请求响应时间将被拉的很长。
因为Filter是同步的,只有当Filter中的任务完成后才会放行用户的请求,而这个Filter中有两处耗时操作:请求第三方接口、数据入库,这无疑增加了用户的等待时间。

因此,我需要将耗时操作异步执行,减少Filter的阻塞时间。

我把这两个耗时操作放入一个新线程中,只要请求一来,就创建一条新线程去处理这两步操作。和先前的方式比对之后发现,确实响应速度提高了不少!

但仔细一想,发现不妙。这种方式没办法控制线程的数量,当访问量很高的情况下,线程数量将会无限增加,这时候会搞垮服务器的!

所以需要一个机制来管理所有的线程,于是我就设计了一个消息队列模型。

模型设计

这里写图片描述
这个模型很简单,由一个任务队列和多个工作线程组成。生产者只需不停地往任务队列中添加任务,消费者(工作线程)不停地从任务队列的另一端取任务执行。

这个模型在项目中的应用是这样的:当一个请求被Filter拦截后,Filter从请求中获取用户的各项信息,然后把这些信息封装成一个任务对象,扔给任务队列,此刻这个Filter的使命就完成了,它完全不用管任务的执行过程。工作线程会不停地从任务队列中取任务执行。

类图设计

这里写图片描述

从代码层面来看,整个消息队列由三个类构成:

消息队列类MsgQueue

这个类管理整个消息队列的运行,是主控程序,它包含以下方法:

  • init:初始化整个消息队列
    在初始化过程中,它会依次做以下事情:

    1. 创建一个任务队列
    2. 调用initWorkThread函数,创建指定数量的工作线程(工作线程一旦被创建,就会不停地读取任务队列中的任务)
    3. 调用loadTask函数,从数据库中加载所有任务
  • loadTask:加载数据库中的所有任务
    这是一个抽象函数,若要使用这个消息队列,必须实现这个函数。
    消息队列初始化的时候会调用这个函数,从数据库中加载上次没有执行完的任务。
    作为消息队列来讲,它并不知道你提供的任务是啥,因此它没办法知道你的任务应该存在哪里,以何种形式存储?因此,这个过程就需要让消息队列使用者自己去实现。

  • saveTask:持久化当前任务队列中的任务
    这也是个抽象函数,若要使用这个消息队列,也必须实现这个函数。
    当使用者调用消息队列的stop函数时,它会被执行,用于存储当前消息队列中尚未被执行的任务,并且在下次启动消息队列的时候通过loadTask函数再次加载进任务队列,这样能确保所有任务不会被遗漏。

  • addTask:向任务队列添加一个任务

  • stop:停止所有工作线程
  • initWorkThread:初始化所有工作线程
    这是一个私有函数,当初始化整个消息队列的时候被init函数调用。

工作线程类WorkThread

工作线程会不断地检查任务队列中是否有任务,若有任务,就会取一个任务执行;若没有任务,就会等待一定时间后再次检查。
它是MsgQueue的一个内部类。因为WorkThread的行为完全由MsgQueue管理,外界不需要知道它的存在。

任务类Task

它是一个接口,并且只有一个函数run,用于封装任务具体的执行过程。

附上代码

以下代码还没将消息队列单独抽象出来,相当于是一个专门用于IP向省市县转化的消息队列,有空把它整一下。
代码中有详细的注释来解释线程安全性问题。

  • 消息队列主控程序
package com.sdata.foundation.web.filter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;

import org.apache.log4j.Logger;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.context.support.XmlWebApplicationContext;

import com.sdata.foundation.web.service.util.DelDataUtilService;
import com.sdata.foundation.web.service.util.InsertDataUtilService;
import com.sdata.foundation.web.service.util.QueryDataUtilService;
import com.thinkgem.jeesite.modules.sys.service.LogService;

/**
 * 记录请求IP的消息队列
 * @author Chai
 *
 */
public class RecordLocationMQ {
   
    // 工作线程的个数
    private static int MaxWorkThread;
    // 工作线程队列
    private static List<WorkThread> workThreadQueue = new ArrayList<WorkThread>();
    // 任务队列(存放等待执行的任务)
    private static List<RecordLocationTask> msgQueue = new ArrayList<RecordLocationTask>();
    // 控制所有工作线程的运行与否
    private static boolean isRunning = true;
    private static LogService LogService;
    // 数据库查询的service(用于任务的持久化)<
  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值