封装一个阻塞队列,轻松实现排队执行任务功能!

前言

个人觉得队列的使用在项目开发中挺多地方可以用到的,所以将如何封装一个队列的过程记录下来,总体来说难度并不大,但毕竟能力有限,如果各位有好的建议或意见欢迎提出来,如果本文能帮到你的话,记得点赞哦。

需求背景

在项目开发中,会经常遇到一些需要排队执行的功能,比如发动态时上传多张图片,需要一张一张的上传,比如直播间动画连需发送或者收到消息需要展示时,需要一个一个动画去展示等等场景,这时候会容易想到用队列去实现,但是我想不少小伙伴会直接弄一个 list,存着要执行的任务,然后通过递归的方式去遍历列表实现,类似下面代码:

//存着一些执行任务的列表
List<String> list = new ArrayList<>();
//执行任务
private void doTask() {
    if (list.size() > 0) {
        String task = list.get(0);
        doSoming(task);
        list.remove(0);
        doTask();
    }
}
复制代码

首先这种方式实现是可以完成所需要的功能的,面对一些简单的场景来说比较容易想到而且实现也简单。但是面对复杂一点的场景却有着不少缺点,特别是在一些祖传项目里面,谁也说不准代码有多乱,到时候维护是很困难的(这个本人已在实际项目中深有体会)。所以封装一个队列去完成这种功能,是一个比较实用而且必须的手段。

首先看看要封装的队列需要有什么功能:

  1. 在实际中,我们执行的任务大概可以分两种,一个是有明确的执行时间的,比如,要连续显示10个动画,每个展示5秒这种。一个是没明确的执行时间的,比如连续上传10张图片,每个上传任务的完成时间是需要等到上传成功回调回来才知道的这种。所以队列第一个功能是每个任务都可以兼容这两种情况,而且当然是一个执行完再执行下一个,排队执行。
  2. 既然要排队执行,当然会有优先级之分,所以每个任务都能设置优先级,队列可以根据优先级去排队执行任务。

至于队列选哪一个,我这里选择的是 PriorityBlockingQueue(阻塞优先级队列),这个队列的特点是存储的对象必须是实现Comparable接口,而且它是阻塞队列,其他特点或者不了解的同学可以自行去了解,接下来都是对它的封装。

1. 定义一个枚举类TaskPriority,定义任务的优先级。

public enum TaskPriority {
    LOW, //低
    DEFAULT,//普通
    HIGH, //高
}
复制代码

优先级分为3种,如注释所示,他们的关系:LOW<DEFAULT<HIGH

2.队列任务执行时间确定和不确定两种情况的实现策略

  1. 针对任务执行时间确定的这种情况,比较简单,可以给这个任务设置一个时间 duration,等任务开始执行时,便开始阻塞等待,等到时间到达时,才放开执行下一个任务。
  2. 针对任务执行时间不确定的情况,我们则需要在任务执行完成的回调里面手动放开,所以这里打算再用一个PriorityBlockingQueue队列,因为它有这样的一个特点:如果队列是空的,调用它的 take()方法时,它就会一直阻塞在那里,当列表不为空时,这个方法就不会阻塞。 所以当任务开始执行时,调用take()方法,等到我们的完成回调来时,再手动给它add一个值,阻塞就放开,再执行下一个任务。

3.确定了任务两种情况的实现策略后,接下来定义一个接口,定义一下每个任务需要做什么事情

public interface ITask extends Comparable<IShowTask> {
    //将该任务插入队列
    void enqueue();

    //执行具体任务的方法
    void doTask();

    //任务执行完成后的回调方法
    void finishTask();

    //设置任务优先级
    IShowTask setPriority(TaskPriority mTaskPriority);

    //获取任务优先级
    TaskPriority getPriority();

    //当优先级相同 按照插入顺序 先入先出 该方法用来标记插入顺序
    void setSequence(int mSequence);

    //获取入队次序
    int getSequence();

    //每个任务的状态,就是标记完成和未完成
    boolean getStatus();
    
    //设置每个任务的执行时间,该方法用于任务执行时间确定的情况
    IShowTask setDuration(int duration);
    
    //获取每个任务执行的时间
    int getDuration();

    //阻塞任务执行,该方法用于任务执行时间不确定的情况
    void blockTask() throws Exception;

    //解除阻塞任务,该方法用于任务执行时间不确定的情况
    void unLockBlock();
}
复制代码

接口基本定义了一些基本需要的方法,因为PriorityBlockingQueue的特点,所以接口继承了Comparable,用于实现优先级排队功能。具体方法功能请看注释。

4.封装一下PriorityBlockingQueue的基本功能

public class BlockTaskQueue {
    private String TAG = "BlockTaskQueue";
    private AtomicInteger mAtomicInteger = new AtomicInteger();
    //阻塞队列
    private final BlockingQueue<IShowTask> mTaskQueue = new PriorityBlockingQueue<>();

    private BlockTaskQueue() {
    }
    //单例模式
    private static class BlockTaskQueueHolder {
        private final static BlockTaskQueue INSTANCE = new BlockTaskQueue();
    }

    public static BlockTaskQueue getInstance() {
        return BlockTaskQueueHolder.INSTANCE;
    }

    /**
     * 插入时 因为每一个Task都实现了comparable接口 所以队列会按照Task复写的compare()方法定义的优先级次序进行插入
     * 当优先级相同时,使用AtomicInteger原子类自增 来为每一个task 设置sequence,
     * sequence的作用是标记两个相同优先级的任务入队的次序
     */
    public <T extends IShowTask> int add(T task) {
        if (!mTaskQueue.contains(task)) {
            task.setSequence(mAtomicInteger.incrementAndGet());
            mTaskQueue.add(task);
            Log.d(TAG, "\n add task " + task.toString());
        }
        return mTaskQueue.size();
    }

    public <T extends IShowTask> void remove(T task) {
        if (mTaskQueue.contains(task)) {
            Log.d(TAG, "\n" + "task has been finished. remove it from task queue");
            mTaskQueue.remove(task);
        }
        if (mTaskQueue.size() == 0) {
            mAtomicInteger.set(0);
        }
    }

    public IShowTask poll() {
        return mTaskQueue.poll();
    }

    public IShowTask take() throws InterruptedException {
        return mTaskQueue.take();
    }

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

    public int size() {
        return mTaskQueue.size();
    }
}
复制代码

这里是一个简单的封装,具体解释请看注释。

5. 写一个类记录下当前执行的任务信息,方便获取时使用

public class CurrentRunningTask {
    private static IShowTask sCurrentShowingTask;

    public static void setCurrentShowingTask(IShowTask task) {
        sCurrentShowingTask = task;
    }

    public static void removeCurrentShowingTask() {
        sCurrentShowingTask = null;
    }

    public static IShowTask getCurrentShowingTask() {
        return sCurrentShowingTask;
    }

    public static boolean getCurrentShowingStatus() {
        return sCurrentShowingTask != null && sCurrentShowingTask.getStatus();
    }
}
复制代码

有时候要获取一些正在执行的任务的信息,所以这里弄类一个类来将正在执行的任务存储起来。

6. 基础需要的东西都写好后,下面开始封装一个基础的任务类了

public class BaseTask implements IShowTask {
    private final String TAG = getClass().getSimpleName();
    private TaskPriority mTaskPriority = TaskPriority.DEFAULT; //默认优先级
    private int mSequence;// 入队次序
    private Boolean mTaskStatus = false; // 标志任务状态,是否仍在展示
    protected WeakReference<BlockTaskQueue> taskQueue;//阻塞队列
    protected int duration = 0; //任务执行时间
    //此队列用来实现任务时间不确定的队列阻塞功能
    private PriorityBlockingQueue<Integer> blockQueue;
    //构造函数
    public BaseTask() {
        taskQueue = new WeakReference<>(BlockTaskQueue.getInstance());
        blockQueue = new PriorityBlockingQueue<>();
    }
    //入队实现
    @Override
    public void enqueue() {
        TaskScheduler.getInstance().enqueue(this);
    }
    //执行任务方法,此时标记为设为true,并且将当前任务记录下来
    @Override
    public void doTask() {
        mTaskStatus = true;
        CurrentRunningTask.setCurrentShowingTask(this);
    }
    //任务执行完成,改变标记位,将任务在队列中移除,并且把记录清除
    @Override
    public void finishTask() {
        this.mTaskStatus = false;
        this.taskQueue.get().remove(this);
        CurrentRunningTask.removeCurrentShowingTask();
        Log.d(TAG, taskQueue.get().size() + "");
    }
    //设置任务优先级实现
    @Override
    public IShowTask setPriority(TaskPriority mTaskPriority) {
        this.mTaskPriority = mTaskPriority;
        return this;
    }
    //设置任务执行时间
    public IShowTask setDuration(int duration) {
        this.duration = duration;
        return this;
    }
    //获取任务优先级
    @Override
    public TaskPriority getPriority() {
        return mTaskPriority;
    }
    //获取任务执行时间
    @Override
    public int getDuration() {
        return duration;
    }
    //设置任务次序
    @Override
    public void setSequence(int mSequence) {
        this.mSequence = mSequence;
    }
    //获取任务次序
    @Override
    public int getSequence() {
        return mSequence;
    }
    获取任务状态
    @Override
    public boolean getStatus() {
        return mTaskStatus;
    }
    //阻塞任务执行
    @Override
    public void blockTask() throws Exception {
        blockQueue.take(); //如果队列里面没数据,就会一直阻塞
    }
    //解除阻塞
     @Override
     public void unLockBlock() {
        blockQueue.add(1); //往里面随便添加一个数据,阻塞就会解除
     }

    /**
     * 排队实现
     * 优先级的标准如下:
     * TaskPriority.LOW < TaskPriority.DEFAULT < TaskPriority.HIGH
     * 当优先级相同 按照插入次序排队
     */
    @Override
    public int compareTo(ITask another) {
        final TaskPriority me = this.getPriority();
        final TaskPriority it = another.getPriority();
        return me == it ? this.getSequence() - another.getSequence() :
                it.ordinal() - me.ordinal();
    }
    //输出一些信息
    @Override
    public String toString() {
        return "task name : " + getClass().getSimpleName() + " sequence : " + mSequence + " TaskPriority : " + mTaskPriority;
    }
}
复制代码

代码并不难,看注释应该都会懂,这里看下入队方法,并没有直接 add 到 taskQueue 里面,而且通过 TaskScheduler 的 enqueue 方法入队,TaskScheduler 是一个任务调度类,里面封装了入队以及对队列操作的一些功能,下面看看它是如何实现的。

7. TaskScheduler

public class TaskScheduler {
    private final String TAG = "TaskScheduler";
    private BlockTaskQueue mTaskQueue = BlockTaskQueue.getInstance();
    private ShowTaskExecutor mExecutor;

    private static class ShowDurationHolder {
        private final static TaskScheduler INSTANCE = new TaskScheduler();
    }

    private TaskScheduler() {
        initExecutor();
    }

    private void initExecutor() {
        mExecutor = new ShowTaskExecutor(mTaskQueue);
        mExecutor.start();
    }

    public static TaskScheduler getInstance() {
        return ShowDurationHolder.INSTANCE;
    }

     public void enqueue(ITask task) {
        //因为TaskScheduler这里写成单例,如果isRunning改成false的话,不判断一下,就会一直都是false
        if (!mExecutor.isRunning()) {
            mExecutor.startRunning();
        }
        //按照优先级插入队列 依次播放
        mTaskQueue.add(task);
    }

    public void resetExecutor() {
        mExecutor.resetExecutor();
    }

    public void clearExecutor() {
        mExecutor.clearExecutor();
    }
}
复制代码

ShowTaskExecutor是一个任务排队执行器,里面主要是一个死循环,不断的在队列里面取出任务,并且执行任务,下面看看它的实现。

8.ShowTaskExecutor

public class ShowTaskExecutor {
    private final String TAG = "ShowTaskExecutor";
    private BlockTaskQueue taskQueue;
    private TaskHandler mTaskHandler;
    private boolean isRunning = true;
    private static final int MSG_EVENT_DO = 0;
    private static final int MSG_EVENT_FINISH = 1;

    public ShowTaskExecutor(BlockTaskQueue taskQueue) {
        this.taskQueue = taskQueue;
        mTaskHandler = new TaskHandler();
    }
    //开始遍历任务队列
    public void start() {
        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    while (isRunning) { //死循环
                        ITask iTask;
                        iTask = taskQueue.take(); //取任务
                        if (iTask != null) {
                            //执行任务
                            TaskEvent doEvent = new TaskEvent();
                            doEvent.setTask(iTask);
                            doEvent.setEventType(TaskEvent.EventType.DO);
                            mTaskHandler.obtainMessage(MSG_EVENT_DO, doEvent).sendToTarget();
                            //一直阻塞,直到任务执行完
                            if (iTask.getDuration()!=0) {
                                TimeUnit.MICROSECONDS.sleep(iTask.getDuration());
                            }else {
                                iTask.blockTask();
                            }
                            //完成任务
                            TaskEvent finishEvent = new TaskEvent();
                            finishEvent.setTask(iTask);
                            finishEvent.setEventType(TaskEvent.EventType.FINISH);
                            mTaskHandler.obtainMessage(MSG_EVENT_FINISH, finishEvent).sendToTarget();
                        }
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
    }
    //根据不同的消息回调不同的方法。
    private static class TaskHandler extends Handler {
        TaskHandler() {
            super(Looper.getMainLooper());
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            TaskEvent taskEvent = (TaskEvent) msg.obj;
            if (msg.what == MSG_EVENT_DO && taskEvent.getEventType() == TaskEvent.EventType.DO) {
                taskEvent.getTask().doTask();
            }
            if (msg.what == MSG_EVENT_FINISH && taskEvent.getEventType() == TaskEvent.EventType.FINISH) {
                taskEvent.getTask().finishTask();
            }
        }
    }

    public void startRunning() {
        isRunning = true;
    }

    public void pauseRunning() {
        isRunning = false;
    }

    public boolean isRunning() {
        return isRunning;
    }

    public void resetExecutor() {
        isRunning = true;
        taskQueue.clear();
    }

    public void clearExecutor() {
        pauseRunning();
        taskQueue.clear();
    }
}
复制代码

好了整个队列已经封装好了,下面看看如何使用吧:

使用方法

  1. 定义一个Task,继承 BaseTask,并实现对应的方法
public class LogTask extends BaseTask {
    String name;

    public LogTask(String name) {
        this.name = name;
    }

    //执行任务方法,在这里实现你的任务具体内容
    @Override
    public void doTask() {
        super.doTask();
        Log.i("LogTask", "--doTask-" + name);
        
        //如果这个Task的执行时间是不确定的,比如上传图片,那么在上传成功后需要手动调用
        //unLockBlock方法解除阻塞,例如:
        uploadImage(new UploadListener{
           void onSuccess(){
                unLockBlock();
            }
        });
    }

    //任务执行完的回调,在这里你可以做些释放资源或者埋点之类的操作
    @Override
    public void finishTask() {
        super.finishTask();
        Log.i("LogTask", "--finishTask-" + name);
    }
}
复制代码
  1. 然后依次入队使用
findViewById(R.id.btn1).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new LogTask("DEFAULT")
                .setDuration(5000) //设置了时间,代表这个任务时间是确定的,如果不确定,则不用设置
                .setPriority(TaskPriority.DEFAULT) //设置优先级,默认是DEFAULT
                .enqueue(); //入队
    }
});
findViewById(R.id.btn2).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new LogTask("LOW")
                .setDuration(4000)
                .setPriority(TaskPriority.LOW)
                .enqueue();
    }
});
findViewById(R.id.btn3).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new LogTask("HIGH")
                .setDuration(3000)
                .setPriority(TaskPriority.HIGH)
                .enqueue();
    }
});
复制代码

就这样就可以了,是不是很简单,随便依次点击按钮多下,你会发现任务都在根据优先级排队执行了。

获取当前运行任务的方法:

LogTask task = (LogTask) CurrentRunningTask.getCurrentShowingTask();
复制代码

猜你喜欢

转载自juejin.im/post/5c2dac66f265da61120522f9