okhttp3连接池清理
okhttp3连接池RealConnectionPool
连接池是用来存储连接实例对象RealConnection。存储RealConnection是为了实现连接复用,提高网络连接工作效率,减少内存开支。
class RealConnectionPool(
taskRunner: TaskRunner,
private val maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) {
private val keepAliveDurationNs: Long = timeUnit.toNanos(keepAliveDuration)
private val cleanupQueue: TaskQueue = taskRunner.newQueue()
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce() = cleanup(System.nanoTime())
}
// 连接池集合
private val connections = ArrayDeque<RealConnection>()
}
可以认为RealConnectionPool在一定程度上也是起了管理连接RealConnection的作用。在面试中有的同学会经常被问到okhttp是怎么实现连接复用的。有时候还会被问到okhttp是怎么清理连接的。这篇文章就是来解析一下连接池是怎么清理连接的
清理在什么时候触发
新建的RealConnection在ExchangeFinder中被创建并确认使用后(这里会涉及到ExchangeFinder.findConnection连接复用的逻辑,本次文章不做探讨),会put到RealConnectionPool的队列中。仔细看put方法,即可找到清理逻辑的痕迹了
fun put(connection: RealConnection) {
this.assertThreadHoldsLock()
connections.add(connection)
cleanupQueue.schedule(cleanupTask)
}
走,去看cleanupTask是个什么东西
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce() = cleanup(System.nanoTime())
}
额…原来cleanupTask是一个匿名内部类。且先不管runOnce是干什么的(一会会给予解释),先看cleanup方法
cleanUp方法
fun cleanup(now: Long): Long {
var inUseConnectionCount = 0
var idleConnectionCount = 0
var longestIdleConnection: RealConnection? = null
var longestIdleDurationNs = Long.MIN_VALUE
synchronized(this) {
for (connection in connections) {
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++
continue
}
idleConnectionCount++
val idleDurationNs = now - connection.idleAtNs
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs
longestIdleConnection = connection
}
}
when {
longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections -> {
connections.remove(longestIdleConnection)
if (connections.isEmpty()) cleanupQueue.cancelAll()
}
idleConnectionCount > 0 -> {
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs
}
inUseConnectionCount > 0 -> {
return keepAliveDurationNs
}
else -> {
return -1
}
}
}
longestIdleConnection!!.socket().closeQuietly()
return 0L
}
咳,如果是不怎么喜欢看源码的同学估计这时候开始嘀咕了:又是一个只会复制源码的狗货。
别急!!!我们把这段代码分开来看!
cleanUp方法的下半部分
不过在看之前,先打个叉。你们知道触发连接池清理的条件是什么吗?很好记的,两个五!
- RealConnection的闲置实现不超过5分钟;
- RealConnectionPool中闲置的RealConnection数量不超过5个;
清理代码最好从较为简单的底部开始理解:
longestIdleDurationNs: 连接队列中最长的闲置时间
idleConnectionCount: 连接队列中处于闲置的总数
inUseConnectionCount:连接队列中仍然在工作的连接总数
我们发现cleanUp方法是带返回的,Long类型。解释一下返回的是什么:下一次执行清理任务的时间。
有了这些线索再去看代码就会清晰很多了。
fun cleanup(now: Long): Long {
var inUseConnectionCount = 0
var idleConnectionCount = 0
var longestIdleConnection: RealConnection? = null
var longestIdleDurationNs = Long.MIN_VALUE
synchronized(this) {
..........上半部的for循环逻辑............
//下半部分
when {
longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections -> {
connections.remove(longestIdleConnection)
if (connections.isEmpty()) cleanupQueue.cancelAll()
}
idleConnectionCount > 0 -> {
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs
}
inUseConnectionCount > 0 -> {
return keepAliveDurationNs
}
else -> {
return -1
}
}
}
longestIdleConnection!!.socket().closeQuietly()
return 0L
}
可能你已经悟了,但我还是要啰嗦的写一下这段代码的内容:
- 如果有Connection的闲置时间超过默认闲置时间,就需要将这个Connection清除(清除顺序是闲置时间大到小,顺序的理由后面讲)。如果Connection闲置的数量超过默认闲置数量,也需要清除,顺序依旧是闲置时间大到小;
- (先解释上一个懵)每次清理只清理一个。但是只要触发清理,返回值就是0,也就是要求再次触发清理任务。由于查找闲置Connection会比较闲置时间,找到闲置最长的Connection。所以说的是从大到小清理。所以注意如果是闲置数量超过的触发清除,可能会只清理到闲置数量小于默认值,就不清除了;
- idleConnectionCount > 0 的情况是什么呢?一、没超过闲置时间 二、闲置数量没超限。所以下一次任务触发时间是默认闲置时间-当前闲置最长时间;
- 没有闲置的Call只要大于0,就继续等默认闲置时间后再来检查;
- 如果没有闲置的Call,就返回-1,表示不需要继续做任务。
那么怎么记录最长闲置时间、闲置数量、在工作数量的呢?这涉及到上半部份的逻辑
cleanUp方法的上半部份
fun cleanup(now: Long): Long {
var inUseConnectionCount = 0
var idleConnectionCount = 0
var longestIdleConnection: RealConnection? = null
var longestIdleDurationNs = Long.MIN_VALUE
synchronized(this) {
for (connection in connections) {
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++
continue
}
idleConnectionCount++
val idleDurationNs = now - connection.idleAtNs
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs
longestIdleConnection = connection
}
}
...........下半部分的内容...............
}
上半部份的逻辑:
- 遍历连接队列,检查连接是处于否闲置,非闲置则inUseConnectionCount++;
- 有闲置则idleConnectionCount++,每找到一个闲置连接就需要通过比对得到闲置时间最长的连接对象。
接下来我们去看看检查连接是否处于闲置的方法pruneAndGetAllocationCount干了什么
pruneAndGetAllocationCount
在看清理代码之前,我们需要知道一件事。RealConnection是一个连接实例,它持有了RealCall对象。是怎么持有的呢?
val calls = mutableListOf<Reference<RealCall>>()
Reference…玛莎嘎…嗯,没错!是一个继承了WeakRefercence的CallReference类(这一段代码就不贴了,可以在RealCall里头找到)。
竟然是弱引用!!!
然后我们继续吧~
private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
val references = connection.calls
var i = 0
while (i < references.size) {
val reference = references[i]
if (reference.get() != null) {
i++
continue
}
// We've discovered a leaked call. This is an application bug.
val callReference = reference as CallReference
val message = "A connection to ${connection.route().address.url} was leaked. " +
"Did you forget to close a response body?"
Platform.get().logCloseableLeak(message, callReference.callStackTrace)
references.removeAt(i)
connection.noNewExchanges = true
// If this was the last allocation, the connection is eligible for immediate eviction.
if (references.isEmpty()) {
connection.idleAtNs = now - keepAliveDurationNs
return 0
}
}
return references.size
}
在Http1.1中,一个RealConnection只会持有一个RealCall,只支持一个Http请求。在Http2.0中,一个RealConnection会持有多个RealCall(一般是128个),支持多个Http请求。
所以对一个RealConnection来说,什么时候是非闲置的?队列size > 0,又由于队列是一个弱引用队列,也就是弱引用队列持有的对象不是null的。所以在最底部return references.size,返回队列大小。
为什么要用WeakReference呢?为什么这个方法内容还不少呢? 其实吧,这里有个特别值得玩味,也是特别值得我们平时使用okhttp需要注意的地方!
使用okhttp注意点
在pruneAndGetAllocationCount方法中会对队列做遍历检查,找到Reference.get==null的对象。其实在源码注释里,我们会看到,Square的工程师提醒我们,这里有泄漏,“Did you forget to close a response body?”,你是否忘记close response body了。对okhttp不熟的读者们这时候肯定会懵逼,卧槽,这个还要关闭?是的,是需要关闭的。使用Retrofit的你可能不太清楚,因为Retrofit已经帮我们实现这段关闭操作了。如果你是自己封装的okhttp,请务必记得要close response body。
而且经过试验也可以发现,如果不主动关闭RealCall释放RealCall,在非HTTP/2的环境下我们就无法使用连接重用,就会生成一大堆的Connection直到出发GC回收RealCall,再到RealConnectionPool做Cleanup()检查,才会清除这些有泄漏的RealCall的RealConnection
看到这里,各位客官可以离开的了。毕竟清理逻辑已经基本搞清楚了。
如果您还想我多说一下,那就再挖一挖清理任务Task和TaskQueue吧
清理任务怎么被执行的
上文一直梳理清理的逻辑。那各位客官知道清理任务是怎么被执行的吗?
猜测一下,执行清理任务应该是放在线程里执行的,而且一般来说既有可能会是放在线程池里执行的。
只是猜测,你有根据吗?有的
class ConnectionPool internal constructor(
internal val delegate: RealConnectionPool
) {
constructor(
maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) : this(RealConnectionPool(
taskRunner = TaskRunner.INSTANCE,
maxIdleConnections = maxIdleConnections,
keepAliveDuration = keepAliveDuration,
timeUnit = timeUnit
))
//我没瞎说吧,两个五:最大闲置数量、最大闲置时间
constructor() : this(5, 5, TimeUnit.MINUTES)
.......忽略部分代码.......
}
TaskRunner.INSTANCE的构建方法中会有一个RealBackend的类。该类初始化阶段会生成线程池(具体代码不贴出来,毕竟这个不是重点)。
让我们回到清理任务开始的地方。
fun put(connection: RealConnection) {
this.assertThreadHoldsLock()
connections.add(connection)
cleanupQueue.schedule(cleanupTask)//cleanupQueue是TaskQueue类的对象
}
调用TaskQueue.schedule(task: Task)方法开始执行清理任务。那就去看看TaskQueue吧
TaskQueue
class TaskQueue internal constructor(
internal val taskRunner: TaskRunner,
internal val name: String
) {
.....省略部分代码.....
//activeTask正在运行的Task(作用一会就说)
internal var activeTask: Task? = null
//futureTasks存储着等待被执行的Task
internal val futureTasks = mutableListOf<Task>()
.....省略部分代码.....
//schedule方法入口
fun schedule(task: Task, delayNanos: Long = 0L) {
synchronized(taskRunner) {
//这一段是在执行前做中断判断,可以忽略
if (shutdown) {
if (task.cancelable) {
taskLog(task, this) { "schedule canceled (queue is shutdown)" }
return
}
taskLog(task, this) { "schedule failed (queue is shutdown)" }
throw RejectedExecutionException()
}
//对传进来的任务及执行时间做处理
if (scheduleAndDecide(task, delayNanos, recurrence = false)) {
//scheduleAndDecide返回true,那就可以开始执行了。
//也就意味着scheduleAndDecide是个编排执行顺序的方法,顺序是按照时间来定的
taskRunner.kickCoordinator(this)
}
}
}
internal fun scheduleAndDecide(task: Task, delayNanos: Long, recurrence: Boolean): Boolean {
// task 把taskQueue存入Task中,相当于Task知道自己属于哪一个TaskQueue的
task.initQueue(this)
val now = taskRunner.backend.nanoTime()
val executeNanoTime = now + delayNanos
//在futureTasks中查找一下是否当前task已经入队
//有 existingIndex = 序列位置
//无 existingIndex = -1
val existingIndex = futureTasks.indexOf(task)
if (existingIndex != -1) {
//task已在队列中
//如果executeNanoTime大于等于下一次执行时间,就意味着任务已经被编排了,以最短的时间为准
//并return false表示结束
if (task.nextExecuteNanoTime <= executeNanoTime) {
taskLog(task, this) { "already scheduled" }
return false
}
//如果executeNanoTime小于任务编排时间,那就肯定需要以executeNanoTime时间编排
//在编排前,先把这个任务从队列中移除出去
futureTasks.removeAt(existingIndex)
}
//更新任务的编排时间
task.nextExecuteNanoTime = executeNanoTime
taskLog(task, this) {
if (recurrence) "run again after ${formatDuration(executeNanoTime - now)}"
else "scheduled after ${formatDuration(executeNanoTime - now)}"
}
//遍历任务队列,按照时间从小到大的顺序,找到插入序号,将任务重新入队。
var insertAt = futureTasks.indexOfFirst { it.nextExecuteNanoTime - now > delayNanos }
if (insertAt == -1) insertAt = futureTasks.size
futureTasks.add(insertAt, task)
//如果插入的序号是0,就返回true
return insertAt == 0
}
}
总结一下scheduleAndDecide所做的事情:
- 如果需要计划进行的任务在队列里,那就查看一下它的计划执行时间。如果时间比计划还早,那就不做处理,按原定时间进行即可。如果计划执行时间比较晚,那就先把任务从队列中取出;
- 取出任务之后,遍历任务表,重新筛选一个符合时间顺序的位置,把任务塞进这个位置里;
- return true相当于立即执行,如果塞进去的位置在第一位(也就是0),那也是true,否则为false
TaskRunner.kickCoordinator(taskQueue: TaskQueue)准备开始执行
TaskRunner
关于TaskRunner的内容解析,我没有想到特别好的解析方式。所以暂时还是以源码+注释的方式进行解析。如果客官老爷有什么意见建议,可以和我说说。
我们就开始吧
class TaskRunner(
val backend: Backend
) {
.......省略部分代码........
//正在运行的task所在的TaskQueue队列
private val busyQueues = mutableListOf<TaskQueue>()
//准备运行的TaskQueue队列
private val readyQueues = mutableListOf<TaskQueue>()
//TaskQueue在片编排完任务Task的执行序列之后来到这里。
internal fun kickCoordinator(taskQueue: TaskQueue) {
this.assertThreadHoldsLock()
//把当前的taskQueue加入到readyQueue队列中
if (taskQueue.activeTask == null) {
if (taskQueue.futureTasks.isNotEmpty()) {
readyQueues.addIfAbsent(taskQueue)
} else {
readyQueues.remove(taskQueue)
}
}
if (coordinatorWaiting) {
backend.coordinatorNotify(this@TaskRunner)
} else {
//coordinatorWaiting一般为false,所以会执行runnable
//backend(我一度看成了碧咸)是持有线程池的对象,通过execute开始执行
backend.execute(runnable)
}
}
}
所以需要看一下runnable这个对象是干什么的
private val runnable: Runnable = object : Runnable {
override fun run() {
while (true) {
//从awaitTaskToRun方法中获取满足执行条件的Task
val task = synchronized(this@TaskRunner) {
awaitTaskToRun()
} ?: return
logElapsed(task, task.queue!!) {
var completedNormally = false
try {
runTask(task)
completedNormally = true
} finally {
if (!completedNormally) {
backend.execute(this)
}
}
}
}
}
}
awaitTaskToRun这个方法虽然不复杂,但是好长啊。我有点后悔来装逼了…
在进去看这个方法之前,为了避免你会混乱懵逼。我先给你捋一捋。
- 清理任务的任务形式是Task;
- Task会加入TaskQueue的futureTask队列
- TaskQueue会加入到TaskRunner的readyQueues队列中
因此可以大概推测awaitTaskToRun方法是从自己的readyQueues队列中,找到时间最靠前的TaskQueue,再从这个TaskQueue的队列中取出时间最靠前的Task来执行。
走,看看是不是这么一回事。
fun awaitTaskToRun(): Task? {
this.assertThreadHoldsLock()
while (true) {
//准备运行的队列为空的情况,就返回null,这个挺好理解的...
if (readyQueues.isEmpty()) {
return null // Nothing to do.
}
val now = backend.nanoTime()
var minDelayNanos = Long.MAX_VALUE
var readyTask: Task? = null
var multipleReadyTasks = false
//遍历readyQueues
//对于一个TaskQueue来说,它可执行时间最早的task,一定是在futureTasks的队头,也就是0位
//所以这里就是要找到这个最早的task,readyTask指向这个最早的task
eachQueue@ for (queue in readyQueues) {
val candidate = queue.futureTasks[0]
//执行时间修正(避免出现负数时间)
val candidateDelay = maxOf(0L, candidate.nextExecuteNanoTime - now)
when {
// Compute the delay of the soonest-executable task.
candidateDelay > 0L -> {
minDelayNanos = minOf(candidateDelay, minDelayNanos)
continue@eachQueue
}
readyTask != null -> {
multipleReadyTasks = true
break@eachQueue
}
// We have a task to execute when we complete the loop.
else -> {
readyTask = candidate
}
}
}
// Implement the decision.
when {
// We have a task ready to go. Get ready.
readyTask != null -> {
//在awaitTaskToRun返回Task前,会先执行beforeRun方法(这个一会讲干嘛使的)
beforeRun(readyTask)
// Also start another thread if there's more work or scheduling to do.
if (multipleReadyTasks || !coordinatorWaiting && readyQueues.isNotEmpty()) {
backend.execute(runnable)
}
return readyTask
}
.......省略掉部分代码........
}
}
}
在awaitTaskToRun方法返回了一个Task之后,runnable会执行runTask方法。而在runTask方法的后半段,会执行afterRun方法。
// 执行Task的三大方法
private fun beforeRun(task: Task) {
this.assertThreadHoldsLock()
task.nextExecuteNanoTime = -1L
val queue = task.queue!!
queue.futureTasks.remove(task)
readyQueues.remove(queue)
queue.activeTask = task
busyQueues.add(queue)
}
private fun runTask(task: Task) {
this.assertThreadDoesntHoldLock()
val currentThread = Thread.currentThread()
val oldName = currentThread.name
currentThread.name = task.name
var delayNanos = -1L
try {
delayNanos = task.runOnce()
} finally {
synchronized(this) {
afterRun(task, delayNanos)
}
currentThread.name = oldName
}
}
private fun afterRun(task: Task, delayNanos: Long) {
this.assertThreadHoldsLock()
val queue = task.queue!!
check(queue.activeTask === task)
val cancelActiveTask = queue.cancelActiveTask
queue.cancelActiveTask = false
queue.activeTask = null
busyQueues.remove(queue)
if (delayNanos != -1L && !cancelActiveTask && !queue.shutdown) {
queue.scheduleAndDecide(task, delayNanos, recurrence = true)
}
if (queue.futureTasks.isNotEmpty()) {
readyQueues.add(queue)
}
}
懒得在代码里写注释了,哈哈哈哈哈哈。个人觉得这里需要结合起来看比较省时间,所以总结一下大致的逻辑
在梳理结构之前需要知道在线程池的清理任务task是进入了TaskQueue队列里,再有就是TaskQueue需要放到TaskRunner内才能够运行。
TaskQueue自身有一个activeTask的对象,代表着正在运行executing的任务。而Task队列futureTasks,以Task的执行时间作为排序。
TaskRunner自身则是带有了两个队列,两个都是TaskQueue的队列busyQueues和readyQueues
- 一个Task进入到TaskQueue中做好排序,进入TaskQueue的futureTask队列中;
- TaskQueue会调用TaskQueue的kickCoodinator方法,把自己加入到TaskRunner的readyQueue中。运行Runnable;
- 处理任务Task会分为1+3步一是awaitTaskToRun,三是beforeRun、runTask、afterRun
- awaitTaskToRun方法中会遍历readyQueue队列,队列的头部task;
- awaitTaskToRun从readyQueue队列中取TaskQueue,并取Queue首个Task,如果计划执行时间晚于当前时间(也就是时间差<=0)即可执行该Task;
- 找到时间合适的Task开始执行beforeRun;
- beforeRun传入参数是即将运行的Task,Task自身有一个成员变量指向自己所属的TaskQueue,所以在方法中可以拿到TaskQueue,将Task从TaskQueue的futureTask中移除掉,设置成TaskQueue的activeTask,代表着对TaskQueue而言,该Task不再是future,而是active。然后将Task所属的Queue从TaskRunner的readyQueues队列中移除,放到busyQueue中,代表正在运行的队列;
- runTask是最好理解的,运行Task方法并拿到下一次的运行时间。传入到afterRun方法中
- 在afterRun方法中将当前Task的所属Queue从busyQueues中移除。先对下一次运行时间做一下判断,不为-1的就重新加入当前Queue的队列中。最后只要当前Queue的队列不为空,就可以继续放入到TaskRun的readyQueue中
后话
对于普通的开发者而言,okhttp的连接池和平时的开发工作直接关系不大。了解这个的用处可能还是拿来在面试的时候吹牛逼。
但是对于我个人来说,当初是看到了连接池清理的逻辑才了解到response是需要close的,对这个知识点的印象非常深刻。刚好最近想着开始写博客,就以这个为第一篇分享内容吧。
写得不好的地方还请各位客官多多指点了
本次源码参考是okhttp4.4.1版,现在okhttp都是用kotlin来写的了~
参考

1953

被折叠的 条评论
为什么被折叠?



