1. 程式人生 > >Java/Android中的優先順序任務佇列的實踐

Java/Android中的優先順序任務佇列的實踐

剛剛把公司的活幹完,去群裡水,有幾個小夥伴問我怎麼實現佇列,於是乎我來寫一篇吧。本篇文章適用於Java和Android開發者,會從實現一個最簡單的佇列過渡到實現一個帶有優先順序的佇列,保準你可以掌握基本的佇列原理。

佇列的基本理解

用生活中的一個情景來舉個栗子,前段時間很火爆的電視劇《人民的名義》中有一個丁義珍式的視窗大家應該都知道了,我們不說《人民的名義》也不說丁義珍,我們來說說這個辦事視窗。

我們知道在某機構上班期間,視窗一直是開著的,有人去辦事了視窗就開始做事,沒人辦事了視窗就處於等待的狀態,如果去辦事的人特別多,這些辦事的人就必須排隊(我特別討厭在要排隊的地方不排隊的人,呼籲大家如果不是性命攸關的急事,一定要排隊,誰不想早點辦完事呢),視窗也可能是多個,而這些視窗中也可能有一些特別視窗,比如軍人優先辦理。

在說佇列之前說兩個名詞:Task是任務,TaskExecutor是任務執行器。

而我們今天要說的佇列就完全符合某機構這個情況,佇列在有Task進來的時候TaskExecutor就立刻開始執行Task,當沒有Task的時候TaskExecutor就處於一個阻塞狀態,當有很多Task的時候Task也需要排隊,TaskExecutor也可以是多個,並且可以指定某幾個Task優先執行或者滯後執行。

綜上所說我們得出一個這樣的關係:佇列相當於某機構TaskExecutor相當於視窗辦事者就是Task

普通佇列

當然很多機構也沒有設定什麼軍人優先的視窗,所以佇列也有不帶優先順序的佇列,因此我們先來實現一個非優先順序的佇列。

我們常用的佇列介面有:Queue<E>BlockingQueue<E>,基於上面我們說的特點,我們用BlockingQueue<E>來實現任務佇列,BlockingQueue<E>的實現有很多,這裡我們選擇LinkedBlockingQueue<E>

和上述某機構不一樣,某機構可以先有機構,再有視窗,再有辦事者。但是我們寫程式碼的時候,要想寫一個佇列,那麼務必要在佇列中寫TaskExecutor,那麼就得先寫好TaskExecutor類,以此類推就得先有Task類。

因此我們先寫一個Task的介面,也就是辦事的人,我把它設計為介面,方便辦各種不同事的人進來:

// 辦事的人。
public interface ITask {

    // 辦事,我們把辦事的方法給辦事的人,也就是你要辦什麼事,由你自己決定。
    void run();
}

接下來再寫一個TaskExecutor的類,也就是視窗,用來執行Task,認真看註釋,非常有助於理解:

// 視窗
public class TaskExecutor extends Thread {

    // 在視窗拍的隊,這個隊裡面是辦事的人。
    private BlockingQueue<ITask> taskQueue;

    // 這個辦事視窗是否在等待著辦事。
    private boolean isRunning = true;

    public TaskExecutor(BlockingQueue<ITask> taskQueue) {
        this.taskQueue = taskQueue;
    }

    // 下班。
    public void quit() {
        isRunning = false;
        interrupt();
    }

    @Override
    public void run() {
        while (isRunning) { // 如果是上班狀態就待著。
            ITask iTask;
            try {
                iTask = taskQueue.take(); // 叫下一個辦事的人進來,沒有人就等著。
            } catch (InterruptedException e) {
                if (!isRunning) {
                    // 發生意外了,是下班狀態的話就把視窗關閉。
                    interrupt();
                    break; // 如果執行到break,後面的程式碼就無效了。
                }
                // 發生意外了,不是下班狀態,那麼視窗繼續等待。
                continue;
            }

            // 為這個辦事的人辦事。
            iTask.run();
        }
    }
}

這裡要稍微解釋下BlockingQueue<T>#take()方法,這個方法當佇列裡面的item為空的時候,它會一直處於阻塞狀態,當佇列中進入item的時候它會立刻有一個返回值,它就和ServerSocket.accept()方法一樣,所以我們把它放入一個Thread中,以免阻塞呼叫它的執行緒(Android中可能是主執行緒)。

辦事的人和視窗都有了,下面我們封裝一個佇列,也就是某機構,用來管理這些視窗:

// 某機構。
public class TaskQueue {

    // 某機構排的隊,隊裡面是辦事的人。
    private BlockingQueue<ITask> mTaskQueue;
    // 好多視窗。
    private TaskExecutor[] mTaskExecutors;

    // 在開發者new佇列的時候,要指定視窗數量。
    public TaskQueue(int size) {
        mTaskQueue = new LinkedBlockingQueue<>();
        mTaskExecutors = new TaskExecutor[size];
    }

    // 開始上班。
    public void start() {
        stop();
        // 把各個視窗都開啟,讓視窗開始上班。
        for (int i = 0; i < mTaskExecutors.length; i++) {
            mTaskExecutors[i] = new TaskExecutor(mTaskQueue);
            mTaskExecutors[i].start();
        }
    }

    // 統一各個視窗下班。
    public void stop() {
        if (mTaskExecutors != null)
            for (TaskExecutor taskExecutor : mTaskExecutors) {
                if (taskExecutor != null) taskExecutor.quit();
            }
    }

    // 開一個門,讓辦事的人能進來。
    public <T extends ITask> int add(T task) {
        if (!mTaskQueue.contains(task)) {
            mTaskQueue.add(task);
        }
        // 返回排的隊的人數,公開透明,讓外面的人看的有多少人在等著辦事。
        return mTaskQueue.size();
    }
}

某機構、視窗、辦事的人都有了,下面我們就派一個人去一件具體的事,但是上面我的Task是一個介面,所以我們需要用一個類來實現這個介面,來做某一件事:

// 做一件列印自己的id的事。
public class PrintTask implements ITask {

    private int id;

    public PrintTask(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        // 為了儘量模擬視窗辦事的速度,我們這裡停頓兩秒。
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ignored) {
        }

        System.out.println("我的id是:" + id);
    }
}

下面就讓我們模擬的虛擬世界執行一次:

public class Main {

    public static void main(String... args) {
        // 這裡暫時只開一個視窗。
        TaskQueue taskQueue = new TaskQueue(1);
        taskQueue.start();

        for (int i = 0; i < 10; i++) {
            PrintTask task = new PrintTask(i);
            taskQueue.add(task);
        }
    }

}

沒錯,佇列按照我們理想的狀況打印出來了:

我的id是:0
我的id是:1
我的id是:2
我的id是:3
我的id是:4
我的id是:5
我的id是:6
我的id是:7
我的id是:8
我的id是:9

上面我門只開了一個視窗,下面我多開幾個視窗:

public class Main {

    public static void main(String... args) {
        // 開三個視窗。
        TaskQueue taskQueue = new TaskQueue(3);
        taskQueue.start(); // 某機構開始工作。

        for (int i = 0; i < 10; i++) {
            // new 10 個需要辦事的人,並且進入某機構辦事。
            PrintTask task = new PrintTask(i);
            taskQueue.add(task);
        }
    }
}

這裡要說明一下,在初始化的時候我們開了3個視窗,內部的順序應該是這樣的:

某機構的大門開了以後,第一個辦事的人進去到了第一個視窗,第二個辦事的人進去到了第二個視窗,第三個辦事的人進去到了第三個視窗,第四個辦事的人進去排隊在第一位,當第一、第二、第三個視窗中不論哪一個視窗的事辦完了,第四個人就去哪一個視窗繼續辦事,第五個人等待,一次類推。這樣子就達到了佇列同事併發三個任務的效果。

這就是一個普通的佇列,其它的一些特性也是基於此再次封裝的,那麼下面我就基於此再把人物的優先順序加上,也就是我們上面說的特殊視窗->軍人優先!

優先順序佇列

我們排隊等待辦事的時候,來了一個辦事的人,那麼如何判斷這個辦事人是否可以優先辦理呢?那就要判斷它是否具有優先的許可權甚至他可以優先到什麼程度。

所以我們需要讓這個Task有一標誌,那就是優先順序,所以我用一個列舉類標記優先順序:

public enum Priority {
    LOW, // 最低。
    DEFAULT, // 預設級別。
    HIGH, // 高於預設級別。
    Immediately // 立刻執行。
}

這裡我把分了四個等級:最低預設立刻,這個等級肯定要給到我們的辦事的人,也就是Task

public interface ITask {

    void run();

    void setPriority(Priority priority);

    Priority getPriority();
}

可以設定優先順序和可以拿到優先順序。

下面我們要把上面的LinkedBlockingQueue替換成PriorityBlockingQueue<E>,因為它可以自動做到優先順序的比較,它要求泛型<E>,也就是我們的Task必須實現Comparable<E>介面,而Comparable<E>有一個compareTo(E)方法可以對兩個<T>做比較,因此我們的佇列需要改一下實現的方法:

// 某機構。
public class TaskQueue {

    // 某機構排的隊,隊裡面是辦事的人。
    private BlockingQueue<ITask> mTaskQueue;
    // 好多視窗。
    private TaskExecutor[] mTaskExecutors;

    // 在開發者new佇列的時候,要指定視窗數量。
    public TaskQueue(int size) {
        mTaskQueue = new PriorityBlockingQueue<>();
        mTaskExecutors = new TaskExecutor[size];
    }

    ...

然後ITask介面繼承Comparable<E>介面:

public interface ITask extends Comparable<ITask> {

    void run();

    void setPriority(Priority priority);

    Priority getPriority();
}

因為有setPriority(Priority)方法和getPriority()方法和Comparable<E>compareTo(E)方法,所以我們的每一個Task都需要實現這幾個方法,這樣就會很麻煩,所以我們封裝一個BasicTask:

public abstract class BasicTask implements ITask {

    // 預設優先順序。
    private Priority priority = Priority.DEFAULT;

    @Override
    public void setPriority(Priority priority) {
        this.priority = priority;
    }

    @Override
    public Priority getPriority() {
        return priority;
    }

    // 做優先順序比較。
    @Override
    public int compareTo(ITask another) {
        final Priority me = this.getPriority();
        final Priority it = another.getPriority();
        return me == it ? [...] : it.ordinal() - me.ordinal();
    }
}

其它都好說,我們看到compareTo(E)方法就不太理解了,這裡說一下這個方法:

compareTo(E)中傳進來的E是另一個Task,如果當前Task比另一個Task更靠前就返回負數,如果比另一個Task靠後,那就返回正數,如果優先順序相等,那就返回0。

這裡要特別注意,我們看到上面當兩個Task優先順序不一樣的時候呼叫了Priority.orinal()方法,並有後面的orinal減去了當前的orinal,怎麼理解呢?首先要理解Priority.orinal()方法,在Java中每一個列舉值都有這個方法,這個列舉的值是它的下標+1,也就是[index + 1],所以我們寫的Priority類其實可以這樣理解:

public enum Priority {
    1,
    2,
    3,
    4
}

繼續,如果給當前Task比較低,給compareTo(E)中的Task設定的優先級別比較高,那麼Priority不一樣,那麼返回的值就是整數,因此當前Task就會被PriorityBlockingQueue<E>排到後面,如果調換那麼返回結果也就調換了。

但是我們注意到me == it ? [...] : it.ordinal() - me.ordinal();中的[...]是什麼鬼啊?因為這裡缺一段程式碼呀哈哈哈(這個作者怎麼傻乎乎的),這一段程式碼的意思是當優先級別一樣的時候怎麼辦,那就是誰先加入佇列誰排到前面唄,那麼怎樣返回值呢,我們怎麼知道哪個Task先加入佇列呢?這個時候可愛的我就出現了,我給它給一個序列標記它什麼時候加入佇列的不久完事了,於是我們可以修改下ITask介面,增加兩個方法:

public interface ITask extends Comparable<ITask> {

    void run();

    void setPriority(Priority priority);

    Priority getPriority();

    void setSequence(int sequence);

    int getSequence();
}

我們用setSequence(int)標記它加入佇列的順序,然後因為setSequence(int)getSequence()是所有Task都需要實現的,所以我們在BasicTask中實現這兩個方法:

public abstract class BasicTask implements ITask {

    // 預設優先順序。
    private Priority priority = Priority.DEFAULT;
    private int sequence;

    @Override
    public void setPriority(Priority priority) {
        this.priority = priority;
    }

    @Override
    public Priority getPriority() {
        return priority;
    }

    @Override
    public void setSequence(int sequence) {
        this.sequence = sequence;
    }

    @Override
    public int getSequence() {
        return sequence;
    }

    // 做優先順序比較。
    @Override
    public int compareTo(ITask another) {
        final Priority me = this.getPriority();
        final Priority it = another.getPriority();
        return me == it ?  this.getSequence() - another.getSequence() :
            it.ordinal() - me.ordinal();
    }
}

看到了吧,剛才的[...]已經變成了this.getSequence() - another.getSequence(),這裡需要和上面的it.ordinal() - me.ordinal();的邏輯對應,上面說到如果給當前Task比較低,給compareTo(E)中的Task設定的優先級別比較高,那麼Priority不一樣,那麼返回的值就是整數,因此當前Task就會被PriorityBlockingQueue<E>排到後面,如果調換那麼返回結果也就調換了。

這裡的邏輯和上面對應就是和上面的邏輯相反,因為這裡是當兩個優先順序一樣時的返回,上面是兩個優先順序不一樣時的返回,所以當優先級別一樣時,返回負數表示當前Task在前,返回正數表示當前Task在後,正好上面上的邏輯對應。

接下來就是給Task設定序列了,於是我們在TaskQueue中的T void add(T)方法做個手腳:

public class TaskQueue {

    private AtomicInteger mAtomicInteger = new AtomicInteger();

    ...

    public TaskQueue(int size) {
        ...
    }

    public void start() {
        ...
    }

    public void stop() {
        ...
    }

    public <T extends ITask> int add(T task) {
        if (!mTaskQueue.contains(task)) {
            task.setSequence(mAtomicInteger.incrementAndGet()); // 注意這行。
            mTaskQueue.add(task);
        }
        return mTaskQueue.size();
    }
}

這裡我們使用了AtomicInteger類,它的incrementAndGet()方法會每次遞增1,其實它相當於:

mAtomicInteger.addAndGet(1);

其它具體用法請自行搜尋,這裡不再贅述。

到此為止,我們的優先級別的佇列就實現完畢了,我們來做下測試:

public static void main(String... args) {
    // 開一個視窗,這樣會讓優先順序更加明顯。
    TaskQueue taskQueue = new TaskQueue(1);
    taskQueue.start(); //  // 某機構開始工作。

    // 為了顯示出優先順序效果,我們預新增3個在前面堵著,讓後面的優先順序效果更明顯。
    taskQueue.add(new PrintTask(110));
    taskQueue.add(new PrintTask(112));
    taskQueue.add(new PrintTask(122));

    for (int i = 0; i < 10; i++) { // 從第0個人開始。
    PrintTask task = new PrintTask(i);
    if (1 == i) { 
        task.setPriority(Priority.LOW); // 讓第2個進入的人最後辦事。
    } else if (8 == i) {
        task.setPriority(Priority.HIGH); // 讓第9個進入的人第二個辦事。
    } else if (9 == i) {
        task.setPriority(Priority.Immediately); // 讓第10個進入的人第一個辦事。
    } 
    // ... 其它進入的人,按照進入順序辦事。
    taskQueue.add(task);
}

沒錯這就是我們看到的效果:

我的id是:9
我的id是:8
我的id是:110
我的id是:112
我的id是:122
我的id是:0
我的id是:2
我的id是:3
我的id是:4
我的id是:5
我的id是:6
我的id是:7
我的id是:1

到這裡就結束啦,本文原始碼下載地址:
http://download.csdn.net/detail/yanzhenjie1003/9841188
專案原始碼使用IDEA寫的,你可以直接import到你的IDEA,或者把原始碼直接拷貝到Eclipse或者AndroidStudio。