一、背景

并发基础学了一段时间,接下来实战一把

业务背景:下载多个文档,如果使用单线程一个个下载,太慢了,我们实现一个并发框架

二、业务要求

可以查询进度,下载了多少个,以及有多少个成功

完成后缓存一段时间,过期清理

三、着手

3.1 接口ITaskProcesser

回想使用其他框架,如spring等,都会提供一些接口,我们开发人员只需要使用对应的接口,即可完成相应的功能。这里我们的并发框架同样提供一个接口,给开发人员实现

public interface ITaskProcesser<T, R> {
	TaskResult<R> taskExecute(T data);
}

其中,T是入参,R是业务方法执行后的结果。taskExecute是业务逻辑,比如下载文档

这些都是开发人员自己实现

3.2 结果类

无论是什么业务,结果都可以概括为三种:业务方法执行结束且成功,业务方法执行结束但失败,业务方法出现异常

public enum TaskResultType {
	//方法成功执行并返回了业务人员需要的结果
	Success,
	//方法成功执行但是返回的是业务人员不需要的结果
	Failure,
	//方法执行抛出了Exception
	Exception;
}

但是这样还不够,比如如果失败了,失败原因是什么。如果成功了,返回结果是什么

所以我们需要再次封装一下

public class TaskResult<R> {

    //这里设置为final,是因为当任务执行完后,这些都是确定的,不需要业务人员去修改
	//方法本身运行是否正确的结果类型
	private final TaskResultType resultType;
	//方法的业务结果数据,使用泛型
	private final R returnValue;
	//如果失败,失败的原因
	private final String reason;
	
	public TaskResult(TaskResultType resultType, R returnValue, String reason) {
		super();
		this.resultType = resultType;
		this.returnValue = returnValue;
		this.reason = reason;
	}
	
	//方便业务人员使用,这个构造方法表示业务方法执行成功返回的结果
	public TaskResult(TaskResultType resultType, R returnValue) {
		super();
		this.resultType = resultType;
		this.returnValue = returnValue;
		this.reason = "Success";
	}

	public TaskResultType getResultType() {
		return resultType;
	}
	public R getReturnValue() {
		return returnValue;
	}
	public String getReason() {
		return reason;
	}

	@Override
	public String toString() {
		return "TaskResult [resultType=" + resultType+ ", returnValue=" + returnValue 
				+ ", reason=" + reason + "]";
	}	
}

这里注意:我们使用了final去修饰属性,因为当任务结束,这些属性都是确定的,不需要让开发人员去修改。同理,只暴露了get方法,并没有set方法

3.3 工作类

每个用户提交给框架的,我们称之为一个工作

一个工作可能包含多个任务

比如A提交一个工作给框架,下载300个文档。B同样可以提交一个工作给框架,下载500个文档

这样的话,工作类就需要以下属性

工作名称

类似于id,用于区分A员工提交的工作和B员工提交的工作

工作任务个数

比如A员工提交的工作下载100个文档,任务个数就是100

工作的任务处理器

比如A员工提交的工作下载100个文档,每个任务是下载,但是B员工提交的工作可能是上传。所以需要开发人员自己实现自己的任务处理器

成功处理的任务数、已处理的任务数

用于反馈给开发人员当前任务进度

结果队列

用于保存每个任务的结果,比如下载100个文档,可能只有80个成功的
这里注意:我们使用阻塞队列保存结果,拿结果从头拿,放结果从尾部放

工作的完成保存的时间

工作完成后不可能一直保存,超过这个时间从缓存中清除,由开发人员自己定义

因此

public class JobInfo<R> {

	//区分唯一的工作
	private final String jobName;
	//工作的任务个数
	private final int jobLength;
	//这个工作的任务处理器(业务人员自己实现自己任务的处理器)
	private final ITaskProcesser<?,?> taskProcesser;
	
	//成功处理的任务数
	private final AtomicInteger  successCount;
	//已处理的任务数
	private final AtomicInteger  taskProcesserCount;
	
	//结果队列,保存工作中每一个任务的结果
//拿结果从头拿,放结果从尾部放
	private final LinkedBlockingDeque<TaskResult<R>> taskDetailQueue;
	//工作完成后,保存的时间,超过这个时间从缓存中清除,由开发人员确定
	private final long expireTime;
	
	//构造函数。阻塞队列应该内部生成,长度为工作的任务个数
	public JobInfo(String jobName, int jobLength, 
			ITaskProcesser<?, ?> taskProcesser,
			long expireTime) {
		super();
		this.jobName = jobName;
		this.jobLength = jobLength;
		this.taskProcesser = taskProcesser;
		this.successCount = new AtomicInteger(0);//初始化工作时应该为0
		this.taskProcesserCount = new AtomicInteger(0);//初始化工作时应该为0
		this.taskDetailQueue = new LinkedBlockingDeque<TaskResult<R>>(jobLength);;
		this.expireTime = expireTime;
	}

	public ITaskProcesser<?, ?> getTaskProcesser() {
		return taskProcesser;
	}

	//返回成功处理的结果数
	public int getSuccessCount() {
		return successCount.get();
	}

	//返回当前已处理的结果数
	public int getTaskProcesserCount() {
		return taskProcesserCount.get();
	}
	
	//提供工作中失败的次数
	public int getFailCount() {
		return taskProcesserCount.get() - successCount.get();
	}
	
    //获取进度
	public String getTotalProcess() {
		return "Success["+successCount.get()+"]/Current["
				+taskProcesserCount.get()+"] Total["+jobLength+"]";
	}
	
	//获得工作中每个任务的处理详情
	public List<TaskResult<R>> getTaskDetail(){
        //新建一个LinkedList返回结果列表,不对外暴露taskDetailQueue
		List<TaskResult<R>> taskList = new LinkedList<>();
		TaskResult<R> taskResult;
		//从阻塞队列中拿任务的结果,反复取,一直取到null为止,说明目前队列中最新的任务结果已经取完,可以不取了
		while((taskResult=taskDetailQueue.pollFirst())!=null) {
			taskList.add(taskResult);
		}
		return taskList;
	}
	
	//放任务的结果,从业务应用角度来说,保证最终一致性即可,不需要对方法加锁.
	public void addTaskResult(TaskResult<R> result,CheckJobProcesser checkJob) {
		if (TaskResultType.Success.equals(result.getResultType())) {
			successCount.incrementAndGet();
		}
		taskDetailQueue.addLast(result);
		taskProcesserCount.incrementAndGet();
		
		if(taskProcesserCount.get()==jobLength) {
			checkJob.putJob(jobName, expireTime);
		}	
	}
}

3.4 框架主体类

1、线程池

既然并发框架,我们首先要使用线程池来提高效率

//线程池的线程数量,保守估计,使用CPU核心数
private static final int THREAD_COUNTS = Runtime.getRuntime().availableProcessors();
//任务队列,需要丢到线程池里处理的任务,这里采用有界的队列,当并发过高,线程池的线程不够用时,缓存任务
private static BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(5000);
//线程池,固定大小,有界队列
private static ExecutorService taskExecutor = 
			new ThreadPoolExecutor(THREAD_COUNTS, THREAD_COUNTS, 60, TimeUnit.SECONDS, taskQueue);

2、框架需要保存用户提交的工作,这里要采用并发安全的容器,并提供返回容器的方法

//存放不同用户提交的工作,采用并发安全的容器
private static ConcurrentHashMap<String, JobInfo<?>> jobInfoMap = new  ConcurrentHashMap<>();

public static Map<String, JobInfo<?>> getMap(){
		return jobInfoMap;
}

3、框架主体类的构造函数(单例)

private PendingJobPool() {}
	
	private static class JobPoolHolder{
		public static PendingJobPool pool = new PendingJobPool();
	}
	
	public static PendingJobPool getInstance() {
		return JobPoolHolder.pool;
	}

4、定期清理工作类,稍后介绍

//任务完成后,在一定的时间供查询,之后为释放资源节约内存,需要定期处理过期的任务
	//定期清理工作类
	private static CheckJobProcesser checkJob = CheckJobProcesser.getInstance();

5、把任务包装成Runnable,提交给线程池执行,并处理任务,写入缓存以供查询

private static class PendingTask<T,R> implements Runnable{
		
		private JobInfo<R> jobInfo;
		private T processData;

		public PendingTask(JobInfo<R> jobInfo, T processData) {
			super();
			this.jobInfo = jobInfo;
			this.processData = processData;
		}

		@SuppressWarnings("unchecked")
		@Override
		public void run() {
			R r = null;
            //获取开发人员写的处理器类
			ITaskProcesser<T,R> taskProcesser = (ITaskProcesser<T, R>) jobInfo.getTaskProcesser();
			TaskResult<R> result = null;
			
			try {
				//调用开发人员实现的具体方法
				result = taskProcesser.taskExecute(processData);
				//要做检查,防止开发人员处理不当
				if (result == null) {
					result = new TaskResult<R>(TaskResultType.Exception, r, 
							"result is null");
				}
				if (result.getResultType() == null) {
					if (result.getReason() == null) {
						result = new TaskResult<R>(TaskResultType.Exception, r, "reason is null");
					} else {
						result = new TaskResult<R>(TaskResultType.Exception, r,
								"result is null,but reason:" + result.getReason());
					}
				} 
			} catch (Exception e) {
				e.printStackTrace();
				result = new TaskResult<R>(TaskResultType.Exception, r, 
						e.getMessage());				
			}finally {
                //执行完成,保存该任务的结果
				jobInfo.addTaskResult(result,checkJob);
			}
		}
	}

6、提供一个获取当前 已有工作的方法

//根据工作名称检索工作
	@SuppressWarnings("unchecked")
	private <R> JobInfo<R> getJob(String jobName){
		JobInfo<R> jobInfo = (JobInfo<R>) jobInfoMap.get(jobName);
		if(null==jobInfo) {
			throw new RuntimeException(jobName+"是个非法任务。");
		}
		return jobInfo;
	}

7、开发人员注册工作

public <R> void registerJob(String jobName,int jobLength,ITaskProcesser<?, ?> taskProcesser,long expireTime) {
		JobInfo<R> jobInfo = new JobInfo(jobName,jobLength,
				taskProcesser,expireTime);
		if (jobInfoMap.putIfAbsent(jobName, jobInfo)!=null) {
			throw new RuntimeException(jobName+"已经注册了!");
		}
	}

8、开发人员提交任务

public <T,R> void putTask(String jobName,T t) {
		JobInfo<R> jobInfo = getJob(jobName);
		PendingTask<T,R> task = new PendingTask<T,R>(jobInfo,t);
		taskExecutor.execute(task);
	}

9、框架返回每个任务的处理结果

public <R> List<TaskResult<R>> getTaskDetail(String jobName){
		JobInfo<R> jobInfo = getJob(jobName);
		return jobInfo.getTaskDetail();
	}

10、框架返回工作的整体处理进度

public <R> String getTaskProgess(String jobName) {
		JobInfo<R> jobInfo = getJob(jobName);
		return jobInfo.getTotalProcess();	
	}

这就是整个框架主体类所有功能

3.5 过期任务队列

工作完成后,不能一直保存在内存中,使用过期队列来缓存一定时间

首先实现一个过期队列的元素数据结构

public class ItemVo<T> implements Delayed{
	
	private long activeTime;//到期时间,单位毫秒
	private T date;
	
	//activeTime是个过期时长
	public ItemVo(long activeTime, T date) {
		super();
		this.activeTime = TimeUnit.NANOSECONDS.convert(activeTime, 
				TimeUnit.MILLISECONDS)+System.nanoTime();//将传入的时长转换为超时的时刻
		this.date = date;
	}
	
	public long getActiveTime() {
		return activeTime;
	}

	public T getDate() {
		return date;
	}

	//按照剩余时间排序
	@Override
	public int compareTo(Delayed o) {
		long d = getDelay(TimeUnit.NANOSECONDS)-o.getDelay(TimeUnit.NANOSECONDS);
		return (d==0)?0:((d>0)?1:-1);
	}

	//返回元素的剩余时间
	@Override
	public long getDelay(TimeUnit unit) {
		long d = unit.convert(this.activeTime-System.nanoTime(),
				TimeUnit.NANOSECONDS);
		return d;
	}
}

然后实现过期队列,同样使用单例模式

public class CheckJobProcesser {
    private static DelayQueue<ItemVo<String>> queue 
    	= new DelayQueue<ItemVo<String>>();//存放已完成任务等待过期的队列
    
	//单例模式------
	private CheckJobProcesser() {}
	
	private static class ProcesserHolder{
		public static CheckJobProcesser processer = new CheckJobProcesser();
	}
	
	public static CheckJobProcesser getInstance() {
		return ProcesserHolder.processer;
	}
	//单例模式------    
    
    //处理队列中到期任务的实行
    private static class FetchJob implements Runnable{

		@Override
		public void run() {
			while(true) {
				try {
					//拿到已经过期的任务
					ItemVo<String> item = queue.take();
					String jobName =  (String)item.getDate();
					PendingJobPool.getMap().remove(jobName);
					System.out.println(jobName+" is out of date,remove from map!");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}			
		}
    }
    
    /*任务完成后,放入队列,经过expireTime时间后,从整个框架中移除*/
    public void putJob(String jobName,long expireTime) {
    	ItemVo<String> item = new ItemVo<String>(expireTime,jobName);
    	queue.offer(item);
    	System.out.println("Job["+jobName+"已经放入了过期检查缓存,过期时长:"+expireTime);
    }
    
    static {
    	Thread thread = new Thread(new FetchJob());
    	thread.setDaemon(true);
    	thread.start();
    	System.out.println("开启任务过期检查守护线程................");
    } 
}

3.6 测试

我们先实现接口,以随机数来模拟下载文档

小于300的随机数,表示下载成功

300~400,表示下载失败

大于400,表示出现异常

public class MyTask implements ITaskProcesser<Integer,Integer> {

	@Override
	public TaskResult<Integer> taskExecute(Integer data) {
		Random r = new Random();
		int flag = r.nextInt(500);
		SleepTools.ms(flag);
		if(flag<=300) {//正常处理的情况
			Integer returnValue = data.intValue()+flag;
			return new TaskResult<Integer>(TaskResultType.Success,returnValue);
		}else if(flag>301&&flag<=400) {//处理失败的情况
			return new TaskResult<Integer>(TaskResultType.Failure,-1,"Failure");
		}else {//发生异常的情况
			try {
				throw new RuntimeException("异常发生了!!");
			} catch (Exception e) {
				return new TaskResult<Integer>(TaskResultType.Exception,
						-1,e.getMessage());
			}
		}
	}
}

测试

public class AppTest {
	
	private final static String JOB_NAME = "计算数值";
	private final static int JOB_LENGTH = 1000;
	
	//查询任务进度的线程
	private static class QueryResult implements Runnable{
		
		private PendingJobPool pool;

		public QueryResult(PendingJobPool pool) {
			super();
			this.pool = pool;
		}

		@Override
		public void run() {
			int i=0;//查询次数
			while(i<350) {
				List<TaskResult<String>> taskDetail = pool.getTaskDetail(JOB_NAME);
				if(!taskDetail.isEmpty()) {
					System.out.println(pool.getTaskProgess(JOB_NAME));
					System.out.println(taskDetail);					
				}
				SleepTools.ms(100);
				i++;
			}
		}
		
	}

	public static void main(String[] args) {
		MyTask myTask = new MyTask();
		//拿到框架的实例
		PendingJobPool pool = PendingJobPool.getInstance();
		//注册job
		pool.registerJob(JOB_NAME, JOB_LENGTH, myTask,1000*5);
		Random r = new Random();
		for(int i=0;i<JOB_LENGTH;i++) {
			//依次推入Task
			pool.putTask(JOB_NAME, r.nextInt(1000));
		}
		Thread t = new Thread(new QueryResult(pool));
		t.start();
	}
}

四、总结

对于框架使用者,也就是开发人员

  1. 重写ITaskProcesser接口,实现自己的业务处理逻辑,比如下载文件
  2. 获取框架实例(采用了单例模式)
  3. 在框架中注册工作,registerJob方法,注册成功才能提交任务,以防多次注册工作
  4. 提交任务,putTask方法,交给线程池完成,并将结果保存到工作中的结果队列中,计算已处理总数,并且当任务全部处理完,会将该工作加入到等待过期队列
  5. 然后就可以获取工作的整体处理进度,getTaskProgess方法
  6. 等待过期队列通过一个循环,判断任务是否过期,过期后清除