Okhttp原理之线程池
线程的概念
- 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
线程的特点
- 在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体
线程的属性
- 轻量级(线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述,线程调度速度相对较快)
- 独立调度和分配(在多线程os中,线程是可以独立运行的基本单位)
- 并发性执行(一个进程包含多个线程)
- 共享进程资源 (一个进程包含多个线程)
进程和线程都是一个时间段的描述,是 CPU 工作时间段的描述,只是颗粒不同罢了。
线程池的概念
- 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。
线程池的优点
1.降低内存消耗:通过复用线程,降低线程的创建以及销毁带来的损耗
2.提高响应速度:当任务到达,无需等待线程创建可以立即执行,跟第一点类似
3.提高管理型:由于线程占用系统资源,如果无限制的创建,不仅会消耗系统的资源,还会降低系统的稳定性,使用线程池可以进行统一的管理,如资源的分配、性能优化等。
线程池的源码分析
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters and default thread factory.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1.int corePoolSize(核心线程数):线程池创建线程的时候,核心线程数将被保留在线程池中,即便当前核心线程数处于(闲置状态),除非设置了allowCoreThreadTimeOut为true,那么如果核心线程数处于(闲置状态),在超过一定时间(keepAliveTime),就会被销毁掉。
2.int maximumPoolSize(线程池最大容量):线程池最大容量=核心线程数+非核心线程数
3.long keepAliveTime (通指非核心线程闲置存活时间):非核心线程空闲时长超过一定时长keepAliveTime,将会被回收
4.TimeUnit unit(keepAliveTime的时间单位)
5.BlockingQueue<Runnable> workQueue(任务队列)
- 当超过核心线程数的时候,新添加的任务会被添加到任务队列中处理。
ThreadPoolExecutor.executor()
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
- 当队列已满,则新建非核心线程,注意,这里入参是false,代表使用非核心线程数,具体源码可以查看
ThreadPoolExecutor.addWorker
ThreadPoolExecutor.addWorker()
else if (!addWorker(command, false))
- 如果添加非核心线程任务失败了,我们就知道我们已经关闭或者饱和了所以拒绝这个任务。可以理解为拒绝策略
else if (!addWorker(command, false))
reject(command);
6.ThreadFactory threadFactory (创建线程的工厂模式)
使用默认即可
Executors.defaultThreadFactory()
7.RejectedExecutionHandler handler(拒绝策略):执行被阻止时要使用的处理程序,因为达到线程边界和队列容量
8.拒绝策略的4种模式
- AbortPolicy:默认策略,在拒绝任务时,会抛出RejectedExecutionException。
- CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务
- DiscardOldestPolicy:该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
- 该策略默默的丢弃无法处理的任务,不予任何处理。
9.图解线程池工作模式
线程池工作模式
10.常见线程池
常见线程池
11 常用的workQueue任务队列
-
SynchronousQueue:线程等待队列。同步队列,按序排队,先来先服务,并且不保留任务。为了避免当线程数达到maximumPoolSize造成的错误,所以maximumPoolSize通常设置无限大Integer.MAX_VALUE。
-
LinkedBlockingQueue:队列接受任务的时候,判断当前线程数是否小于核心线程数,如果小于,则新建核心线程处理任务,如果等于核心线程数,则进入队列等待。由于队列没有最大值的限制,所以超过核心线程数的任务,都会添加到队列任务中。所以,这种模式下maximumPoolSize的设置无效,因为总线程数=核心+非核心。
-
ArrayBlockingQueue:可以设置队列长度,跟LinkedBlockingQueue类似,最大区别,当前队列添加满的时候,会新建非核心线程执行任务,当核心+非核心> maximumPoolSize(线程总数),并且队列也满了,则抛出异常。
-
DelayQueue:队列入队,达到延迟时间后,执行任务,主要用来执行延迟任务操作
线程跟线程池的关系
- 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
合理配置线程池
1.cpu密集型:线程池中线程个数尽量少,常规配置(CPU核心数+1)
2.io密集型:有io操作速度比cpu速度慢,所以运行这种类型的任务时,cpu处于空闲状态,那么线程池可以多分配线程数量,以此提高cpu利用率,常规配置(2*CPU核心数+1)
3.乱七八糟任务:视情况进行拆解
Okhttp线程池的使用
- Dispatcher
@get:JvmName("executorService") val executorService: ExecutorService
get() {
if (executorServiceOrNull == null) {
executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
}
return executorServiceOrNull!!
}
根据源码可知,Okhttp使用SynchronousQueue,对标上文,当前线程池没有核心线程,同步队列,先来先执行,线程空闲后,不会保留,并且适用于短时间的任务操作。
-
SynchronousQueue每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此队列内部其实没有任何一个元素,或者说容量为0,严格说并不是一种容器,由于队列没有容量,因此不能调用peek等操作,因此只有移除元素才有新增的元素,显然这是一种快速传递元素的方式,也就是说在这种情况下元素总是以最快的方式从插入者(生产者)传递给移除者(消费者),这在多任务队列中最快的处理任务方式。对于高频请求场景,无疑是最合适的。
-
在OKHttp中,创建了一个阀值是Integer.MAX_VALUE的线程池,它不保留任何最小线程,随时创建更多的线程数,而且如果线程空闲后,只能多活60秒。所以也就说如果收到20个并发请求,线程池会创建20个线程,当完成后的60秒后会自动关闭所有20个线程。他这样设计成不设上限的线程,以保证I/O任务中高阻塞低占用的过程,不会长时间卡在阻塞上。