1. 程式人生 > >高效易用的okio(四)

高效易用的okio(四)

超時機制在我們的日常生活中隨處可見,最為常見就是火車了,如果你不能按時到達火車站點,那麼你就錯失坐這一趟火車的機會

在前面的文章,就已經提到過 okio 中的有一個超時機制 Timeout, 現在就來說說它的原理

okio 中的超時機制只要就兩種:

  1. 同步超時 Timeout
  2. 非同步超時 AsyncTimeout

還有一個超時物件 ForwardingTimeout ,不過這個屬於一個空盒子,需要裝入其他的 Timeout 物件才可以使用

同步超時 TimeOut

所謂超時,就是用來控制某個任務執行的最大時長,當執行超過指定的時間時,任務將被中斷

例如當從輸入流 Source 讀取資料超時後,輸入流將被關閉,任務到此結束

而在 Timeout 中,主要使用兩個判斷條件來判斷任務是否超時了:

  1. 任務設定了結束時間( hasDeadline = true )並且當前已經過了結束時間( deadlineNanoTime )
  2. 任務已經過了超時時間( timeoutNanos )

正是下面的變數:

public class Timeout {
	.....
 	/**
     * 是否設定了結束時間
     */
    private boolean hasDeadline;
    /**
     * 結束時間
     */
private long deadlineNanoTime; /** * 超時時間 */ private long timeoutNanos; ..... }

Timeout 裡面,用於判斷超時的方法主要是有兩個,一個是 throwIfReached

    /**
     *
     * 該方法並不是檢測超時方法
     * 該方法用於檢測執行緒是否中斷了或者是否到了結束時間
     * 如果是,那麼就丟擲異常來進行中斷
     * 目前用於在執行讀寫操作時的檢查
     */
    public void throwIfReached
() throws IOException { if (Thread.interrupted()) { throw new InterruptedIOException("thread interrupted"); } //判斷是否設定了結束flag以及是否到了結束時間 if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) { throw new InterruptedIOException("deadline reached"); } }

正如上面寫的,這個方法是在執行讀寫操作時,判斷一下執行緒是否中斷或當前任務是否已經到了結束時間的

通過對該方法的全域性搜尋,可以大致明白這個用途:

在這裡插入圖片描述

在這裡插入圖片描述

這時候是不是覺得有點奇怪,怎麼還有一個 timeoutNanos 沒有用到的?

別急,接下來介紹的 waitUntilNotified 就需要用到它了

waitUntilNotified 用來等待某個指定的 monitor 物件,直到這個物件被 notify 或者超時時間到 為止:

public final void waitUntilNotified(Object monitor) throws InterruptedIOException {
        try {
            //獲取當前是否設定結束flag
            boolean hasDeadline = hasDeadline();
            //獲取超時時間
            long timeoutNanos = timeoutNanos();
            //當沒有設定超時時間,那麼就設定 wait ,它將無限等待直到物件被 notify 為止
            if (!hasDeadline && timeoutNanos == 0L) {
                monitor.wait(); 
                return;
            }
            //根據timeoutNanos和deadlineNanoTime計算出較短的超時時間waitNanos
            //也就是okio需要等待多久
            long waitNanos;
            //當前系統時間
            long start = System.nanoTime();
            if (hasDeadline && timeoutNanos != 0) {
                //如果設定了結束flag並且超時時間不為0
                //先計算下還有多久到結束時間
                long deadlineNanos = deadlineNanoTime() - start;
                //對比結束時間,超時時間,那個時間段更加短,取短的值
                waitNanos = Math.min(timeoutNanos, deadlineNanos);
            } else if (hasDeadline) {
                waitNanos = deadlineNanoTime() - start;
            } else {
                waitNanos = timeoutNanos;
            }
            //呼叫wait方法並設定等待超時時間
            //直到了超時了或者 monitor 給 notify 為止
            long elapsedNanos = 0L;
            if (waitNanos > 0L) {
                long waitMillis = waitNanos / 1000000L;
                monitor.wait(waitMillis, (int) (waitNanos - waitMillis * 1000000L));
                //記錄跑到這裡過去了多少時間,用於計算超時
                elapsedNanos = System.nanoTime() - start;
            }
            // 走到這裡,說明wait等待超時時間到,或者 monitor 給 notify了
            if (elapsedNanos >= waitNanos) {
                //如果時超時時間到就丟擲InterruptedIOException異常
                throw new InterruptedIOException("timeout");
            }
        } catch (InterruptedException e) {
            throw new InterruptedIOException("interrupted");
        }
    }

這裡使用了 Java 的 waitnotify 機制,這種常用在同步機制上面

非同步超時 AsyncTimeout

非同步超時 AsyncTimeout 繼承自 TimeOut ,相比起它的父類 TimeOutAsyncTimeout 多了一個守護執行緒 Watchdog 和需要自定義一個 timeOut 方法

例如在 okio 的原始碼中,就提供了一個自定義 timeOut 方法,用於任務超時後,關閉 Socket

private static AsyncTimeout timeout(final Socket socket) {
	 return new AsyncTimeout() {
         ...........
          @Override
            protected void timedOut() {
                try {
                    socket.close();
                } catch (Exception e) {
                    logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
                } catch (AssertionError e) {
                    if (isAndroidGetsocknameError(e)) {
                        // Catch this exception due to a Firmware issue up to android 4.2.2
                        // https://code.google.com/p/android/issues/detail?id=54072
                     logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
                    } else {
                        throw e;
                    }
                }
            }
	 }
}

瞭解了 AsyncTimeout 自定義需要注意的事項後,我們來看下它裡面的具體原理,先來了解下 Watchdog 這個守護程序到底是幹什麼的:

在這裡插入圖片描述

可以看到,所有的 AsyncTimeout 在會組成一個連結串列,而 Watchdog 則是無限迴圈去取出裡面的元素,只要發現超時的元素就會執行 timedOut 方法
連結串列的定義是在類的開頭:

public class AsyncTimeout extends Timeout {
    ..........
    /**
     * 連結串列的頭節點,指向連結串列中第一個元素,也就是head.next為連結串列的第一個元素
     * 如果head.next為null,那麼這是一個空的佇列
     */
    static @Nullable AsyncTimeout head;
    
     /**
     * The next node in the linked list.
     * 當前 AsyncTimeout 指向的下一個連結串列元素
     */
    private @Nullable AsyncTimeout next;
    ..........
}

當連結串列不為空時, 就是 Watchdog 工作的時候了:

 private static final class Watchdog extends Thread {
        Watchdog() {
            super("Okio Watchdog");
            setDaemon(true);//設定為守護程序
        }

        @Override
        public void run() {
            while (true) {
                try {
                    AsyncTimeout timedOut;
                    synchronized (AsyncTimeout.class) {
                        timedOut = awaitTimeout();
                        //沒有找到需要結束的節點,繼續迴圈查詢
                        if (timedOut == null) {
                            continue;
                        }
                        //如果只能找到頭指標,那麼說明這個佇列為null
                        if (timedOut == head) {
                            head = null;
                            return;
                        }
                    }
                    //找到發生超時的元素,執行它的自定義超時回撥方法timedOut,
                    //注意,這裡不能做耗時操作,否則會阻塞連結串列中其他已經發生超時的元素
                    timedOut.timedOut();
                } catch (InterruptedException ignored) {
                }
            }
        }
    }

首先需要明確的是,守護執行緒耗費資源是非常低,當只剩下守護執行緒時,jvm 也就關閉了,因此這裡的死迴圈並沒有消耗太多的記憶體

然後就是 awaitTimeout,這個方法用來查詢當前超時的元素,我們來看下它的怎麼去找的:

static AsyncTimeout awaitTimeout() throws InterruptedException {
        //獲取連結串列的第一個元素
        AsyncTimeout node = head.next;

        if (node == null) {
            // 連結串列為空,則最長等待 IDLE_TIMEOUT_NANOS 時間
            long startNanos = System.nanoTime();
            AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS);
            return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS
                    ? head  // The idle timeout elapsed.
                    : null; // The situation has changed.
        }

        long waitNanos = node.remainingNanos(System.nanoTime());

        // 連結串列的第一個元素還沒有超時,繼續等待超時時間到
        if (waitNanos > 0) {
            long waitMillis = waitNanos / 1000000L;
            waitNanos -= (waitMillis * 1000000L);
            AsyncTimeout.class.wait(waitMillis, (int) waitNanos);
            return null;
        }

        // 連結串列的第一個元素超時時間到,從連結串列中移除並將其返回
        head.next = node.next;
        node.next = null;
        return node;
    }

它是通過 head 去找的,這也說明了 連結串列的排序是按照超時時間的從低到高開始排序的

每次都是去找 head.next 節點,超時了就執行 timeOut 方法,沒有就繼續 wait

不過,當節點執行了 wait 之後,是什麼時候 notify 呢?通過程式碼追蹤,找到了 scheduleTimeout 方法以及 enter 方法 和 exit 方法

enter 方法是進入連結串列的方法, exit 是退出連結串列的方法,是執行讀寫操作都會呼叫它們:

在這裡插入圖片描述

enter 方法用於插入連結串列元素 :

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;
        scheduleTimeout(this, timeoutNanos, hasDeadline);
    }

它最終是呼叫了 scheduleTimeout 方法:

private static synchronized void scheduleTimeout(
            AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
        // 當連結串列為空時,初始化連結串列並啟動守護執行緒
        if (head == null) {
            head = new AsyncTimeout();
            new Watchdog().start();
        }
        // 計算超時的時間點timeoutAt
        long now = System.nanoTime();
        if (timeoutNanos != 0 && hasDeadline) {
            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();
        }

        long remainingNanos = node.remainingNanos(now);
        //從連結串列頭開始遍歷連結串列
        for (AsyncTimeout prev = head; true; prev = prev.next) {
            //當到達連結串列尾部,或者根據超時時間從短到長排序找到合適位置後插入連結串列
            if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
                node.next = prev.next;
                prev.next = node;
                //如果當前插入的節點是連結串列的第一個元素,那麼需要喚醒在awaitTimeout方法中的wait操作
                if (prev == head) {
                    //喚醒操作
                    AsyncTimeout.class.notify(); 
                }
                break;
            }
        }
    }

exit 方法用於刪除連結串列中的元素:

final void exit(boolean throwOnTimeout) throws IOException {
        boolean timedOut = exit();
        if (timedOut && throwOnTimeout) throw newTimeoutException(null);
}

public final boolean exit() {
        if (!inQueue) return false;
        inQueue = false;
        return cancelScheduledTimeout(this);
}

private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {
        // Remove the node from the linked list.
        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!
        return true;
    }

可以看到,到最後就是簡單的連結串列資料的刪除操作

總結

到此,這次 okio 框架的解析就結束了,雖然還有部分功能和模組沒有講到,不過大致的流程原理都已經過了一遍了

對比 Java 的原生 IO,可以看出 okio 使用更加方便,更加高效的記憶體利用,建議在實際開發中都用上它