深入了解Android Okio的超時(shí)機(jī)制
Okio是一個(gè)IO庫,底層基于Java原生的輸入輸出流實(shí)現(xiàn)。但原生的輸入輸出流并沒有提供超時(shí)的檢測(cè)機(jī)制。而Okio實(shí)現(xiàn)了這個(gè)功能。建議讀者先閱讀 Android | 徹底理解 Okio 之源碼篇 ,然后再閱讀本篇內(nèi)容會(huì)更好理解。
Timeout 類的設(shè)計(jì)
探討超時(shí)機(jī)制,首先要了解Timeout
這個(gè)類。Timeout
實(shí)現(xiàn)了Okio的同步超時(shí)檢測(cè),這里的同步指的是“任務(wù)執(zhí)行”和“超時(shí)檢測(cè)”是同步的,有順序的。同步超時(shí)不會(huì)直接中斷任務(wù)執(zhí)行,它首先會(huì)檢查是否發(fā)生超時(shí),然后決定是否中斷任務(wù)執(zhí)行。throwIfReached
就是一個(gè)同步超時(shí)檢測(cè)的方法。
理解 timeout 與 deadline 的區(qū)別
timeout
中文意為“超時(shí)”,deadline
中文意為“最后期限”,它們是有明顯區(qū)別的。 Timeout
類中有一系列的timeoutXxx
方法,timeoutXxx
是用來設(shè)置**一次操作完成的最大等待時(shí)間。若這個(gè)操作在等待時(shí)間內(nèi)沒有結(jié)束,則認(rèn)為超時(shí)。 deadlineXxx
系列方法則是用來設(shè)置一項(xiàng)任務(wù)完成的最大等待時(shí)間。**意味著在未來多長(zhǎng)時(shí)間內(nèi),需要將這項(xiàng)任務(wù)完成,否則認(rèn)為超時(shí)。它可能包含一次或多次的操作。
讀取文件的例子
回顧下之前Okio讀取文件例子。
public void readFile() { try { FileInputStream fis = new FileInputStream("test.txt"); okio.Source source = Okio.source(fis); BufferedSource bs = Okio.buffer(source); source.timeout().deadline(1, TimeUnit.MILLISECONDS); String res = bs.readUtf8(); System.out.println(res); } catch (Exception e){ e.printStackTrace(); } }
在這個(gè)例子中,我們使用deadline
設(shè)置了超時(shí)時(shí)間為1ms,這意味著從現(xiàn)在開始,讀取文件的這項(xiàng)任務(wù),必須在未來的1ms內(nèi)完成,否則認(rèn)為超時(shí)。而讀取文件的這項(xiàng)任務(wù),就包含了多次的文件讀取操作。
搖骰子的例子
我們?cè)賮砜聪旅孢@個(gè)搖骰子的程序。Dice
是一個(gè)骰子類,roll
方法表示搖骰子,搖出來的點(diǎn)數(shù)latestTotal
不會(huì)超過12。rollAtFixedRate
會(huì)開啟一個(gè)線程,每隔一段時(shí)間調(diào)用roll
方法搖一次骰子。awaitTotal
方法會(huì)當(dāng)骰子的點(diǎn)數(shù)與我們傳遞進(jìn)去的total
值一樣或者超時(shí)而結(jié)束。
private class Dice { Random random = new Random(); int latestTotal; // 搖骰子 public synchronized void roll() { latestTotal = 2 + random.nextInt(6) + random.nextInt(6); System.out.println("Rolled " + latestTotal); notifyAll(); } // 開啟一個(gè)線程,每隔一段時(shí)間執(zhí)行 roll 方法 public void rollAtFixedRate(int period, TimeUnit timeUnit) { Executors.newScheduledThreadPool(0).scheduleAtFixedRate(new Runnable() { public void run() { roll(); } }, 0, period, timeUnit); } // 超時(shí)檢測(cè) public synchronized void awaitTotal(Timeout timeout, int total) throws InterruptedIOException { while (latestTotal != total) { timeout.waitUntilNotified(this); } } }
timeout()
是一個(gè)測(cè)試骰子類的方法,在主線程中運(yùn)行。該程序設(shè)置每隔3s搖一次骰子,主線程設(shè)置超時(shí)時(shí)間為6s,期望搖到的點(diǎn)數(shù)是20。因?yàn)樵O(shè)置的超時(shí)是timeoutXxx
系列的方法,所以這里超時(shí)的意思是“只要我搖一次骰子的時(shí)間不超過6s,那么我就不會(huì)超時(shí),可以一直搖骰子”。因?yàn)閾u出骰子的最大點(diǎn)數(shù)是12,而期望值是20,永遠(yuǎn)也搖不出來20這個(gè)點(diǎn)數(shù),且搖一次骰子的時(shí)間是3s多,也不滿足超時(shí)的時(shí)間。所以主線程就會(huì)一直處于等待狀態(tài)。
public void timeout(){ try { Dice dice = new Dice(); dice.rollAtFixedRate(3, TimeUnit.SECONDS); Timeout timeout = new Timeout(); timeout.timeout(6, TimeUnit.SECONDS); dice.awaitTotal(timeout, 20); } catch (Exception e) { e.printStackTrace(); } }
現(xiàn)在將timeout()
方法修改一下,將timeout.timeout(6, TimeUnit.SECONDS)
改為timeout.deadline(6, TimeUnit.SECONDS)
,之前我們說過deadlineXxx
設(shè)置的超時(shí)**意味著在未來多長(zhǎng)時(shí)間內(nèi),需要將這項(xiàng)任務(wù)完成。**在搖骰子這里的意思就是“從現(xiàn)在開始,我只可以搖6s的骰子。超過這個(gè)時(shí)間你還在搖,則認(rèn)為超時(shí)”。它關(guān)注的是可以搖多久的骰子,而不是搖一次骰子不能超過多久的時(shí)間。
public void timeout(){ try { Dice dice = new Dice(); dice.rollAtFixedRate(3, TimeUnit.SECONDS); Timeout timeout = new Timeout(); timeout.deadline(6, TimeUnit.SECONDS); dice.awaitTotal(timeout, 20); } catch (Exception e) { e.printStackTrace(); } }
上述程序,主線程會(huì)在6s后因超時(shí)而停止等待,結(jié)束運(yùn)行。
等待直到喚醒
前面舉了兩個(gè)例子讓大家理解Okio中timeout
和deadline
的區(qū)別。在搖骰子的例子中用到了waitUntilNotified
這個(gè)方法來檢測(cè)超時(shí),中文意思為“等待直到喚醒”。也就是Java多線程中經(jīng)典的“等待-喚醒”機(jī)制,該機(jī)制常常用于多線程之間的通信。調(diào)用waitUntilNotified
方法的線程會(huì)一直處于等待狀態(tài),除非被喚醒或者因超時(shí)而拋出異常。下面是該方法的源碼。
public final void waitUntilNotified(Object monitor) throws InterruptedIOException { try { boolean hasDeadline = hasDeadline(); long timeoutNanos = timeoutNanos(); // 若沒有設(shè)置 deadline && timeout,則一直等待直到喚醒 if (!hasDeadline && timeoutNanos == 0L) { monitor.wait(); // There is no timeout: wait forever. return; } // Compute how long we'll wait. // 計(jì)算等待的時(shí)長(zhǎng),若同時(shí)設(shè)置了deadline 和 timeout,則 deadline 優(yōu)先 long waitNanos; long start = System.nanoTime(); if (hasDeadline && timeoutNanos != 0) { long deadlineNanos = deadlineNanoTime() - start; waitNanos = Math.min(timeoutNanos, deadlineNanos); } else if (hasDeadline) { waitNanos = deadlineNanoTime() - start; } else { waitNanos = timeoutNanos; } // Attempt to wait that long. This will break out early if the monitor is notified. long elapsedNanos = 0L; if (waitNanos > 0L) { long waitMillis = waitNanos / 1000000L; // 等待 waitNanos monitor.wait(waitMillis, (int) (waitNanos - waitMillis * 1000000L)); // 計(jì)算從等待 waitNanos 到喚醒所用時(shí)間 elapsedNanos = System.nanoTime() - start; } // Throw if the timeout elapsed before the monitor was notified. // 若等待了 waitNanos 還沒喚醒,認(rèn)為超時(shí) if (elapsedNanos >= waitNanos) { throw new InterruptedIOException("timeout"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Retain interrupted status. throw new InterruptedIOException("interrupted"); } }
查看waitUntilNotified
的源碼,我們發(fā)現(xiàn)該方法基于“等待-通知”機(jī)制,添加了多線程之間的超時(shí)檢測(cè)功能,一個(gè)線程用來執(zhí)行具體的任務(wù),一個(gè)線程調(diào)用該方法來檢測(cè)超時(shí)。在Okio中的管道就使用了waitUntilNotified
這個(gè)方法。
AsyncTimeout 類的設(shè)計(jì)
AsyncTimeout
內(nèi)部維護(hù)一個(gè)單鏈表,節(jié)點(diǎn)的類型是AsyncTimeout
,以到超時(shí)之前的剩余時(shí)間升序排序,即超時(shí)的剩余時(shí)間越大,節(jié)點(diǎn)就在鏈表越后的位置。對(duì)鏈表的操作,使用了synchronized
關(guān)鍵字加類鎖,保證在同一時(shí)間,只有一個(gè)線程可以對(duì)鏈表進(jìn)行修改訪問操作。
AsyncTimeout
實(shí)現(xiàn)了Okio的異步超時(shí)檢測(cè)。這里的異步指的是“任務(wù)執(zhí)行”和“超時(shí)檢測(cè)”是異步的,在執(zhí)行任務(wù)的同時(shí),也在進(jìn)行任務(wù)的“超時(shí)檢測(cè)”。你會(huì)覺得這和上面搖骰子的例子很像,一個(gè)線程執(zhí)行任務(wù),一個(gè)線程檢測(cè)超時(shí)。事實(shí)上,AsyncTimeout
也正是這樣實(shí)現(xiàn)的,它內(nèi)部的Watchdog
線程就是用來檢測(cè)超時(shí)的。當(dāng)我們要對(duì)一次操作或一項(xiàng)任務(wù)設(shè)置超時(shí),使用成對(duì)的enter()
和exit()
,模板代碼如下。
enter(); // do something exit();
若上面do something
的操作超時(shí),timedOut()
方法將會(huì)在Watchdog
線程被回調(diào)??梢钥匆姡@種包裹性的模板代碼,靈活性很大,我們幾乎可以在其中放置任何想要檢測(cè)超時(shí)的一個(gè)或多個(gè)操作。
AsyncTimeout 成員變量
下面是AsyncTimeout
類主要的成員變量。
private static final long IDLE_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(60); static @Nullable AsyncTimeout head; private boolean inQueue; private @Nullable AsyncTimeout next; private long timeoutAt;
IDLE_TIMEOUT_MILLIS
,在單鏈表中沒有節(jié)點(diǎn)時(shí),Watchdog
線程等待的時(shí)間head
,單鏈表的頭結(jié)點(diǎn),是一個(gè)虛假節(jié)點(diǎn)。當(dāng)鏈表中只存在該節(jié)點(diǎn),認(rèn)為該鏈表為空。inQueue
,當(dāng)前節(jié)點(diǎn)是否在鏈表中。next
,當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)。timeoutAt
,以當(dāng)前時(shí)間為基準(zhǔn),當(dāng)前節(jié)點(diǎn)在將來何時(shí)超時(shí)。
AsyncTimeout 成員方法
scheduleTimeout 有序的將超時(shí)節(jié)點(diǎn)加入到鏈表中
scheduleTimeout
方法可以將一個(gè)超時(shí)節(jié)點(diǎn)按照超時(shí)的剩余時(shí)間有序的插入到鏈表當(dāng)中。注意該方法使用synchronized
修飾,是一個(gè)同步方法,可以保證對(duì)鏈表的操作是線程安全的。
private static synchronized void scheduleTimeout(AsyncTimeout node, long timeoutNanos, boolean hasDeadline) { // Start the watchdog thread and create the head node when the first timeout is scheduled. // 若 head 節(jié)點(diǎn)為 null, 初始化 head 并啟動(dòng) Watchdog 線程 if (head == null) { head = new AsyncTimeout(); new Watchdog().start(); } // 計(jì)算 node 節(jié)點(diǎn)的 timeoutAt 值 long now = System.nanoTime(); if (timeoutNanos != 0 && hasDeadline) { // Compute the earliest event; either timeout or deadline. Because nanoTime can wrap around, // Math.min() is undefined for absolute values, but meaningful for relative ones. node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now); } else if (timeoutNanos != 0) { node.timeoutAt = now + timeoutNanos; } else if (hasDeadline) { node.timeoutAt = node.deadlineNanoTime(); } else { throw new AssertionError(); } // Insert the node in sorted order. // 返回 node 節(jié)點(diǎn)的超時(shí)剩余時(shí)間 long remainingNanos = node.remainingNanos(now); // 從 head 節(jié)點(diǎn)開始遍歷鏈表, 將 node 節(jié)點(diǎn)插入到合適的位置 for (AsyncTimeout prev = head; true; prev = prev.next) { // 若當(dāng)前遍歷的節(jié)點(diǎn)下一個(gè)節(jié)點(diǎn)為 null 或者 node 節(jié)點(diǎn)的超時(shí)剩余時(shí)間小于下一個(gè)節(jié)點(diǎn) if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) { // 將 node 節(jié)點(diǎn)插入到鏈表 node.next = prev.next; prev.next = node; // 若當(dāng)前遍歷的節(jié)點(diǎn)是 head, 喚醒 watchdog 線程 if (prev == head) { AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front. } break; } } }
Watchdog 線程
在scheduleTimeout
方法中,若head
為null
,則會(huì)初始化head
并啟動(dòng)Watchdog
線程。Watchdog
是一個(gè)守護(hù)線程,因此它會(huì)隨著JVM進(jìn)程的結(jié)束而結(jié)束。前面我們說過Watchdog
線程是用來檢測(cè)超時(shí)的,它會(huì)逐個(gè)檢查鏈表中的超時(shí)節(jié)點(diǎn)是否超時(shí),直到鏈表中所有節(jié)點(diǎn)檢查完畢后結(jié)束運(yùn)行。
private static final class Watchdog extends Thread { Watchdog() { super("Okio Watchdog"); setDaemon(true); } public void run() { while (true) { try { // 超時(shí)的節(jié)點(diǎn) AsyncTimeout timedOut; // 加鎖,同步代碼塊 synchronized (AsyncTimeout.class) { // 等待節(jié)點(diǎn)超時(shí) timedOut = awaitTimeout(); // Didn't find a node to interrupt. Try again. // 當(dāng)前該節(jié)點(diǎn)沒有超時(shí),繼續(xù)檢查 if (timedOut == null) continue; // The queue is completely empty. Let this thread exit and let another watchdog thread // get created on the next call to scheduleTimeout(). // 鏈表中已經(jīng)沒有超時(shí)節(jié)點(diǎn),結(jié)束運(yùn)行 if (timedOut == head) { head = null; return; } } // Close the timed out node. // timedOut 節(jié)點(diǎn)超時(shí),回調(diào) timedOut() 方法 timedOut.timedOut(); } catch (InterruptedException ignored) { } } } }
awaitTimeout 等待節(jié)點(diǎn)超時(shí)
在Watchdog
線程中會(huì)調(diào)用awaitTimeout
方法來等待檢測(cè)的節(jié)點(diǎn)超時(shí),若檢測(cè)的節(jié)點(diǎn)沒有超時(shí),該方法返回null
。否則返回超時(shí)的節(jié)點(diǎn)。
static @Nullable AsyncTimeout awaitTimeout() throws InterruptedException { // Get the next eligible node. // 檢測(cè)的節(jié)點(diǎn) AsyncTimeout node = head.next; // The queue is empty. Wait until either something is enqueued or the idle timeout elapses. // 若鏈表為空 if (node == null) { long startNanos = System.nanoTime(); // Watchdog 線程等待 60s,期間會(huì)釋放類鎖 AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS); // 等待 60s 后若鏈表還為空則返回 head,否則返回 null return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS ? head // The idle timeout elapsed. : null; // The situation has changed. } // node 節(jié)點(diǎn)超時(shí)剩余的時(shí)間 long waitNanos = node.remainingNanos(System.nanoTime()); // The head of the queue hasn't timed out yet. Await that. // node 節(jié)點(diǎn)超時(shí)剩余的時(shí)間 > 0,說明 node 還未超時(shí),繼續(xù)等待 waitNanos 后返回 null if (waitNanos > 0) { // Waiting is made complicated by the fact that we work in nanoseconds, // but the API wants (millis, nanos) in two arguments. long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); AsyncTimeout.class.wait(waitMillis, (int) waitNanos); return null; } // The head of the queue has timed out. Remove it. // node 節(jié)點(diǎn)超時(shí)了,將 node 從鏈表中移除并返回 head.next = node.next; node.next = null; return node; }
enter 進(jìn)入超時(shí)檢測(cè)
分析完上面三個(gè)方法后再來看enter
就非常的簡(jiǎn)單了,enter
內(nèi)部調(diào)用了scheduleTimeout
方法來添加一個(gè)超時(shí)節(jié)點(diǎn)到鏈表當(dāng)中,而Watchdog
線程隨即會(huì)開始檢測(cè)超時(shí)。
public final void enter() { if (inQueue) throw new IllegalStateException("Unbalanced enter/exit"); long timeoutNanos = timeoutNanos(); boolean hasDeadline = hasDeadline(); if (timeoutNanos == 0 && !hasDeadline) { return; // No timeout and no deadline? Don't bother with the queue. } // 更新 inQueue 為 true inQueue = true; scheduleTimeout(this, timeoutNanos, hasDeadline); }
exit 退出超時(shí)檢測(cè)
前面說過,enter
和exit
在檢測(cè)超時(shí)是需要成對(duì)出現(xiàn)的。它們之間的代碼就是需要檢測(cè)超時(shí)的代碼。exit
方法的返回值表示enter
和exit
中間檢測(cè)的代碼是否超時(shí)。
public final boolean exit() { if (!inQueue) return false; // 更新 inQueue 為 false inQueue = false; return cancelScheduledTimeout(this); }
cancelScheduledTimeout
方法會(huì)將當(dāng)前的超時(shí)節(jié)點(diǎn)從鏈表中移除。為了保證對(duì)鏈表的操作是線程安全的,該方法也是一個(gè)同步方法。我們知道在awaitTimeout
方法中,若某個(gè)節(jié)點(diǎn)超時(shí)了會(huì)將它從鏈表中移除。那么當(dāng)調(diào)用cancelScheduledTimeout
發(fā)現(xiàn)node
不在鏈表中,則一定表明node
超時(shí)了。
private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) { // Remove the node from the linked list. // 若 node 在鏈表中,將其移除。 for (AsyncTimeout prev = head; prev != null; prev = prev.next) { if (prev.next == node) { prev.next = node.next; node.next = null; return false; } } // The node wasn't found in the linked list: it must have timed out! // node 不在鏈表中,則 node 一定超時(shí)了,返回 true return true; }
總結(jié)
本文詳細(xì)講解了Okio中超時(shí)機(jī)制的實(shí)現(xiàn)原理,主要是Timeout
和AsyncTimeout
類的源碼分析與解讀。相信大家已經(jīng)掌握了這部分知識(shí),現(xiàn)總結(jié)一下文中要點(diǎn)。
- Okio 基于等待-喚醒機(jī)制,使用
Watchdog
線程來檢測(cè)超時(shí)。 - 當(dāng)要對(duì)某項(xiàng)操作或任務(wù)進(jìn)行超時(shí)檢測(cè)時(shí),將它們放到
enter
和exit
的中間。 - Okio 對(duì)鏈表的使用非常頻繁,在文件讀寫和超時(shí)檢測(cè)都使用到了鏈表這個(gè)結(jié)構(gòu)。
以上就是深入了解Android Okio的超時(shí)機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于Android Okio超時(shí)機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中通過view方式獲取當(dāng)前Activity的屏幕截圖實(shí)現(xiàn)方法
這篇文章主要介紹了Android中通過view方式獲取當(dāng)前Activity的屏幕截圖實(shí)現(xiàn)方法,本文方法相對(duì)簡(jiǎn)單,容易理解,需要的朋友可以參考下2014-09-09Android開發(fā)實(shí)現(xiàn)AlertDialog中View的控件設(shè)置監(jiān)聽功能分析
這篇文章主要介紹了Android開發(fā)實(shí)現(xiàn)AlertDialog中View的控件設(shè)置監(jiān)聽功能,結(jié)合實(shí)例形式分析了Android針對(duì)AlertDialog中的控件使用View進(jìn)行監(jiān)聽的相關(guān)操作技巧,需要的朋友可以參考下2017-11-11android app判斷是否有系統(tǒng)簽名步驟詳解
這篇文章主要為大家介紹了android app判斷是否有系統(tǒng)簽名步驟詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11Android自定義view實(shí)現(xiàn)列表內(nèi)左滑刪除Item
這篇文章主要介紹了微信小程序列表中item左滑刪除功能,本文分步驟給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-02-02Android仿網(wǎng)易客戶端頂部導(dǎo)航欄效果
這篇文章主要為大家詳細(xì)介紹了Android仿網(wǎng)易客戶端頂部導(dǎo)航欄效果,幫助大家制作網(wǎng)易客戶端導(dǎo)航欄特效,感興趣的小伙伴們可以參考一下2016-06-06