OkHttp3源碼解析(三)——連接池復(fù)用

本文基于OkHttp3的3.11.0版本

implementation 'com.squareup.okhttp3:okhttp:3.11.0'

我們已經(jīng)分析了OkHttp3的攔截器鏈和緩存策略胸哥,今天我們再來看看OkHttp3的連接池復(fù)用赡鲜。

客戶端和服務(wù)器建立socket連接需要經(jīng)歷TCP的三次握手和四次揮手,是一種比較消耗資源的動(dòng)作嘲更。Http中有一種keepAlive connections的機(jī)制揩瞪,在和客戶端通信結(jié)束以后可以保持連接指定的時(shí)間赋朦。OkHttp3支持5個(gè)并發(fā)socket連接李破,默認(rèn)的keepAlive時(shí)間為5分鐘。下面我們來看看OkHttp3是怎么實(shí)現(xiàn)連接池復(fù)用的毛嫉。

OkHttp3的連接池--ConnectionPool

public final class ConnectionPool {
    
    //線程池妇菱,用于執(zhí)行清理空閑連接
    private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
    //最大的空閑socket連接數(shù)
    private final int maxIdleConnections;
    //socket的keepAlive時(shí)間
    private final long keepAliveDurationNs;
    
    private final Deque<RealConnection> connections = new ArrayDeque<>();
    final RouteDatabase routeDatabase = new RouteDatabase();
    boolean cleanupRunning;
}

ConnectionPool里的幾個(gè)重要變量:

(1)executor線程池,類似于CachedThreadPool闯团,用于執(zhí)行清理空閑連接的任務(wù)。

(2)Deque雙向隊(duì)列浪讳,同時(shí)具有隊(duì)列和棧的性質(zhì)涌萤,經(jīng)常在緩存中被使用,里面維護(hù)的RealConnection是socket物理連接的包裝

(3)RouteDatabase负溪,用來記錄連接失敗的路線名單

下面看看ConnectionPool的構(gòu)造函數(shù)

public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
}

public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

    // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
    if (keepAliveDuration <= 0) {
      throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
    }
}

從構(gòu)造函數(shù)中可以看出,ConnectionPool的默認(rèn)空閑連接數(shù)為5個(gè)辐真,keepAlive時(shí)間為5分鐘。ConnectionPool是什么時(shí)候被創(chuàng)建的呢侍咱?是在OkHttpClient的builder中:

public static final class Builder {
    ...
    ConnectionPool connectionPool;
    ...
    public Builder() {
        ...
        connectionPool = new ConnectionPool();
        ...
    }
    
    //我們也可以定制連接池
    public Builder connectionPool(ConnectionPool connectionPool) {
        if (connectionPool == null) throw new NullPointerException("connectionPool == null");
        this.connectionPool = connectionPool;
        return this;
    }
}

緩存操作:添加楔脯、獲取、回收連接

(1)從緩存中獲取連接

//ConnectionPool.class
@Nullable 
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
}

獲取連接的邏輯比較簡單昧廷,就遍歷連接池里的連接connections堪嫂,然后用RealConnection的isEligible方法找到符合條件的連接木柬,如果有符合條件的連接則復(fù)用。需要注意的是眉枕,這里還調(diào)用了streamAllocation的acquire方法。acquire方法的作用是對(duì)RealConnection引用的streamAllocation進(jìn)行計(jì)數(shù)寂玲,OkHttp3是通過RealConnection的StreamAllocation的引用計(jì)數(shù)是否為0來實(shí)現(xiàn)自動(dòng)回收連接的梗摇。

//StreamAllocation.class
public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}

public static final class StreamAllocationReference extends WeakReference<StreamAllocation> {

    public final Object callStackTrace;

    StreamAllocationReference(StreamAllocation referent, Object callStackTrace) {
      super(referent);
      this.callStackTrace = callStackTrace;
    }
}
//RealConnection.class
public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

每一個(gè)RealConnection中都有一個(gè)allocations變量,用于記錄對(duì)于StreamAllocation的引用伶授。StreamAllocation中包裝有HttpCodec流纹,而HttpCodec里面封裝有Request和Response讀寫Socket的抽象。每一個(gè)請求Request通過Http來請求數(shù)據(jù)時(shí)都需要通過StreamAllocation來獲取HttpCodec漱凝,從而讀取響應(yīng)結(jié)果,而每一個(gè)StreamAllocation都是和一個(gè)RealConnection綁定的愕乎,因?yàn)橹挥型ㄟ^RealConnection才能建立socket連接壁公。所以StreamAllocation可以說是RealConnection、HttpCodec和請求之間的橋梁紊册。

當(dāng)然同樣的StreamAllocation還有一個(gè)release方法,用于移除計(jì)數(shù)芳绩,也就是將當(dāng)前的StreamAllocation的引用從對(duì)應(yīng)的RealConnection的引用列表中移除。

private void release(RealConnection connection) {
    for (int i = 0, size = connection.allocations.size(); i < size; i++) {
      Reference<StreamAllocation> reference = connection.allocations.get(i);
      if (reference.get() == this) {
        connection.allocations.remove(i);
        return;
      }
    }
    throw new IllegalStateException();
}

(2)向緩存中添加連接

//ConnectionPool.class
void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

添加連接之前會(huì)先調(diào)用線程池執(zhí)行清理空閑連接的任務(wù)铺浇,也就是回收空閑的連接垛膝。

(3)空閑連接的回收

private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
        long waitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
};

cleanupRunnable中執(zhí)行清理任務(wù)是通過cleanup方法來完成,cleanup方法會(huì)返回下次需要清理的間隔時(shí)間倚聚,然后會(huì)調(diào)用wait方法釋放鎖和時(shí)間片凿可。等時(shí)間到了就再次進(jìn)行清理。下面看看具體的清理邏輯:

long cleanup(long now) {
    //記錄活躍的連接數(shù)
    int inUseConnectionCount = 0;
    //記錄空閑的連接數(shù)
    int idleConnectionCount = 0;
    //空閑時(shí)間最長的連接
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //判斷連接是否在使用惨驶,也就是通過StreamAllocation的引用計(jì)數(shù)來判斷
        //返回值大于0說明正在被使用
        if (pruneAndGetAllocationCount(connection, now) > 0) {
            //活躍的連接數(shù)+1
            inUseConnectionCount++;
            continue;
        }
        //說明是空閑連接敛助,所以空閑連接數(shù)+1
        idleConnectionCount++;

        //找出了空閑時(shí)間最長的連接,準(zhǔn)備移除
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        //如果空閑時(shí)間最長的連接的空閑時(shí)間超過了5分鐘
        //或是空閑的連接數(shù)超過了限制续扔,就移除
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        //如果存在空閑連接但是還沒有超過5分鐘
        //就返回剩下的時(shí)間焕数,便于下次進(jìn)行清理
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        //如果沒有空閑的連接,那就等5分鐘后再嘗試清理
        return keepAliveDurationNs;
      } else {
        //當(dāng)前沒有任何連接识脆,就返回-1善已,跳出循環(huán)
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
}

下面我們看看判斷連接是否是活躍連接的pruneAndGetAllocationCount方法

private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List<Reference<StreamAllocation>> references = connection.allocations;
    for (int i = 0; i < references.size(); ) {
        Reference<StreamAllocation> reference = references.get(i);
    
        //如果存在引用,就說明是活躍連接雕拼,就繼續(xù)看下一個(gè)StreamAllocation
        if (reference.get() != null) {
            i++;
            continue;
        }

      // We've discovered a leaked allocation. This is an application bug.
      //發(fā)現(xiàn)泄漏的引用,會(huì)打印日志
        StreamAllocation.StreamAllocationReference streamAllocRef =
            (StreamAllocation.StreamAllocationReference) reference;
        String message = "A connection to " + connection.route().address().url()
            + " was leaked. Did you forget to close a response body?";
        Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);
        
        //如果沒有引用偎球,就移除
        references.remove(i);
        connection.noNewStreams = true;

        //如果列表為空,就說明此連接上沒有StreamAllocation引用了袍冷,就返回0猫牡,表示是空閑的連接
        if (references.isEmpty()) {
            connection.idleAtNanos = now - keepAliveDurationNs;
            return 0;
        }
    }
    //遍歷結(jié)束后,返回引用的數(shù)量淌友,說明當(dāng)前連接是活躍連接
    return references.size();
}

至此我們就分析完OkHttp3的連接池復(fù)用了震庭。

總結(jié)

(1)OkHttp3中支持5個(gè)并發(fā)socket連接瑰抵,默認(rèn)的keepAlive時(shí)間為5分鐘器联,當(dāng)然我們可以在構(gòu)建OkHttpClient時(shí)設(shè)置不同的值。

(2)OkHttp3通過Deque<RealConnection>來存儲(chǔ)連接肴颊,通過put渣磷、get等操作來管理連接。

(3)OkHttp3通過每個(gè)連接的引用計(jì)數(shù)對(duì)象StreamAllocation的計(jì)數(shù)來回收空閑的連接祟身,向連接池添加新的連接時(shí)會(huì)觸發(fā)執(zhí)行清理空閑連接的任務(wù)物独。清理空閑連接的任務(wù)通過線程池來執(zhí)行氯葬。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帚称,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌戏羽,老刑警劉巖楼吃,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妄讯,死亡現(xiàn)場離奇詭異酷宵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)炕置,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門男韧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人仍劈,你說我怎么就攤上這事寡壮。” “怎么了况既?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵棒仍,是天一觀的道長。 經(jīng)常有香客問我莫其,道長,這世上最難降的妖魔是什么浇揩? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任憨颠,我火速辦了婚禮,結(jié)果婚禮上爽彤,老公的妹妹穿的比我還像新娘。我一直安慰自己往核,他們只是感情好嚷节,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布蝶缀。 她就那樣靜靜地躺著薄货,像睡著了一般。 火紅的嫁衣襯著肌膚如雪柄慰。 梳的紋絲不亂的頭發(fā)上税娜,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音概行,去河邊找鬼。 笑死凳忙,一個(gè)胖子當(dāng)著我的面吹牛禽炬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播腹尖,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼热幔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绎巨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤肿男,失蹤者是張志新(化名)和其女友劉穎却嗡,沒想到半個(gè)月后嘹承,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡撼港,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了往毡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片靶溜。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖嗤详,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情葱色,我是刑警寧澤娘香,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站舞痰,受9級(jí)特大地震影響诀姚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赫段,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贬丛。 院中可真熱鬧给涕,春花似錦、人聲如沸够庙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剔难。三九已至奥喻,卻和暖如春非迹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背憎兽。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留酒朵,地道東北人扎附。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像留夜,于是被迫代替她去往敵國和親匙铡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子碍粥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容