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。