1. 线程池
基本功能:线程的复用,减少创建和销毁线程的开销
当系统接收到一个任务时,需要一个线程,并不会立刻去创建一个新的线程,会先去线程池中查看是否有空余的线程。此时,若线程池中有空闲的线程,直接使用;若没有,再去创建一个新的线程
任务完成时,也不会直接很简单的销毁线程,而是放入线程池中,等待下次复用
Java线程上下文切换
多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行
多线程是在同一个程序内部并行执行,因此会对相同的内存空间进行并发读写操作。这可能是在单线程程序中从来不会遇到的问题。其中的一些错误也未必会在单CPU机器上出现,因为两个线程从来不会得到真正的并行执行。然而,更现代的计算机伴随着多核CPU的出现,也就意味着 不同的线程能被不同的CPU核得到真正意义的并行执行。
1.1 多核
多核、多CPU、超线程,这三个其实都是CPU架构设计的概念
一个现代CPU除了处理器核心之外还包括 寄存器、L1L2缓存这些存储设备、浮点运算单元、整数运算单元等一些辅助运算设备以及内部总线等
一个多核的CPU也就是一个CPU上有多个处理器核心,这样有什么好处呢?比如说现在我们要在一台计算机上跑一个多线程的程序,因为是一个进程里的线程,所以需要一些共享一些存储变量,如果这台计算机都是单核单线程CPU的话,就意味着这个程序的不同线程需要经常在CPU之间的外部总线上通信,同时还要处理不同CPU之间不同缓存导致数据不一致的问题, 所以在这种场景下多核单CPU的架构就能发挥很大的优势,通信都在内部总线,共用同一个缓存
1.2 多CPU
前面提了多核的好处,那为什么要多CPU呢?这个其实很容易想到,如果要运行多个程序(进程)的话,假如只有一个CPU的话,就意味着要经常进行进程上下文切换,因为单CPU即便是多核的,也只是多个处理器核心,其他设备都是共用的,所以 多个进程就必然要经常进行进程上下文切换,这个代价是很高的
1.3 超线程
超线程这个概念是Intel提出的,简单来说是在一个CPU上真正的并发两个线程
听起来似乎不太可能,因为CPU都是分时的啊,其实这里也是分时, 因为前面也提到一个CPU除了处理器核心还有其他设备,一段代码执行过程也不光是只有处理器核心工作,如果两个线程A和B,A正在使用处理器核心,B正在使用缓存或者其他设备,那AB两个线程就可以并发执行,但是如果AB都在访问同一个设备,那就只能等前一个线程执行完后一个线程才能执行。
实现这种并发的原理是在CPU里加了一个协调辅助核心,根据Intel提供的数据,这样一个设备会使得设备面积增大5%,但是性能提高15%~30%
1.4 多线程
一个进程里多线程之间可以共享变量,线程间通信开销也较小,可以更好的利用多核CPU的性能,多核CPU上跑多线程程序往往会比单线程更快,
有的时候甚至在单核CPU上多线程程序也会有更好的性能,因为 虽然多线程会有上下文切换和线程创建销毁开销,但是单线程程序会被IO阻塞无法充分利用CPU资源,加上线程的上下文开销较低以及线程池的大量应用,多线程在很多场景下都会有更高的效率
1.5 进程与线程
进程是操作系统的管理单位,线程是进程管理单位
不管是在单线程还是多线程中, 每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但为返回的过程)
每个线程共享堆空间,拥有自己独立的栈空间
- 线程划分尺度小于进程,线程隶属于某个进程
- 进程是CPU、内存等资源占用的基本单位,线程是不能独立占有这些资源的
- 进程之间相互独立,通信比较困难,而线程之间共享一块内存区域,通信方便
- 进程在执行过程中,包含比较固定的入口、执行顺序和出口,而进程的这些过程会被应用程序控制
2. 线程上下文切换
- 上下文切换(进程切换或任务切换):指CPU从一个进程或线程切换到另一个进程或线程
- 进程(有时候也称做任务)是指一个程序运行的实例
- 在Linux系统中,线程 就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程
- 上下文 是指某一时间点 CPU 寄存器和程序计数器的内容
- 寄存器 是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度
- 程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统
上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行以下的活动:
- 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处
- 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复
- 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程
在计算机中,多任务指的是同时运行两个或多个程序
在多任务处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。上下文切换就是这样一个过程,他允许CPU记录并恢复各种正在运行程序的状态,使它能够完成切换操作
多任务系统往往需要同时执行多道作业。作业数往往大于机器的CPU数,然而一颗CPU同时只能执行一项任务,如何让用户感觉这些任务正在同时进行呢? 操作系统的设计者 巧妙地利用了时间片轮转的方式, CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务。任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗CPU上执行变成了可能
主要切换原因:
- 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务
- 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务
- 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务
- 用户代码挂起当前任务,让出CPU时间
- 硬件中断
切换损耗
- 直接消耗:CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的 pipeline 需要刷掉
- 间接消耗:多核的 cache 之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小
3. Android 中的线程池
/**
* 根据参数初始化一个线程池
*
* @param corePoolSize 队列未满时,线程最大的并发数
* @param maximumPoolSize 队列满后线程能够达到的最大并发数
* @param keepAliveTime 空闲线程被回收的时间限制
* @param unit keepAliveTime 的时间单位
* @param workQueue 阻塞的队列类型
* @param threadFactory 创建一个新线程的 Factory
* @param handler 超出 maximumPoolSize + workQueue 时,任务会交过
* RejectedExecutetoionHandler 来处理
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
- corePoolSize :核心线程数
默认情况下,核心线程会在线程池中一直存在,即使处于闲置状态。若ThreadPoolExecutor
的allowCoreThreadTimeOut = true
,闲置的核心线程在等待新任务到来时有超时策略。时间间隔由keepAliveTime
确定,当超过指定的时长后,核心线程会被终止
maximumPoolSize :线程池最大线程数;当活动线程数达到数值时,后续新的任务将会被阻塞
keepAliveTime :非核心线程闲置超时时长;闲置的非核心线程空闲时间超过这个限制后,会被回收;若
ThreadPoolExecutor
的allowCoreThreadTimeOut = true
,同样会作用于核心线程unit :时间单位
workQueue :线程池中的任务队列;通过线程池的
execute()
提交的Runnable
对象存储在这个队列中threadFactory :线程工厂;为线程池提供创建新线程的功能。
ThreadFactory
是一个接口,只有一个方法Thread newThread(Runnable r)
AsyncTask 线程池配置
- 核心线程数等于
CPU 核心数 + 1
- 线程池的最大线程数
CPU 核心数 * 2 + 1
- 核心线程无超时机制,非核心线程闲置的时长为
1s
- 任务队列容量为
128
3.1 优点与规则
线程池优点 :
- 重用线程池中的线程,避免线程的创建和销毁所带来的性能开销
- 能有效控制线程池的最大并发数,避免大量的线程之间的因互相抢占系统的资源而导致的阻塞现象
- 能够对线程进行简单的管理,并提供定时的执行以及指定间隔循环执行等功能
大致执行规则 :
- 线程池中的线程数量未达到核心线程的数量,直接启动一个核心线程来执行任务
- 线程池中的线程数量已经达到或者超过核心线程的数量,任务会插入到任务队列的排队等待执行
- 若在步骤
2
中,无法将任务插入到任务队列中,一般是由于任务队列已满。此时,若未到到线程池最大线程数量,会立刻启动一个非核心线程来执行 - 若在步骤
3
中,线程数量已经已经达到线程池规定的最大值,就拒绝执行此任务。ThreadPoolExecutor
调用RejectExecutionHandler
的rejectedExecution()
来通知调用者
3.2 分类
FixedThreadPool 固定线程数的线程池
通过Executors.newFixedThreadPool()
创建
一种线程数量固定的线程池,当线程处于闲置状态也不会被回收,直到线程池关闭
当所有的线程都处于活动状态时,新任务会处于等待状态,直到有线程空闲出来
FixedThreadPool
只有核心线程并且这些核心线程都不会被回收,可以更快的加速地响应外界的请求
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
CachedThreadPool 缓存型线程池
通过Executors.newCachedThreadPool()
创建
线程数量不定的线程池,只有非核心线程,最大线程数为 Integer.MAX_VALUE
当线程池中的线程都处于活动状态时,线程池会创建新的线程来处理任务,否则就利用空闲的线程来处理任务
线程池中的空闲线程都有超时机制,时长60s
适合执行大量的耗时较少的任务
当整个线程池中的任务都处于闲置状态时,线程池的线程都会因超时而终止,此时CachedThreadPool
中实际是没有任何线程的,几乎不占用任何系统资源
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
ScheduledThreadPool 调度型线程池
通过Executors.newScheduledThreadPool()
创建
核心线程数固定的,而非核心线程数没有限制,当非核心线程闲置时会被立即回收
- 主要用于执行定时任务和具有固定周期的重复任务
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
SingleThreadExecutor 单例型线程
通过Executors.newSingleThreadExecutor()
创建
只有一个核心线程,确保所有任务都在一个线程中按顺序执行
意义在于统一将所有的外界任务到一个线程中,在这些任务之间不需要处理线程同步的问题
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}