线程池的地位十分重要,基本上涉及到跨线程的框架都使用到了线程池,比如说OkHttp、RxJava、LiveData以及协程等。
一、与新建一个线程相比,线程池的特点?
节省开销: 线程池中的线程可以重复利用
速度快:任务来了就能开始,省去创建线程的时间
线程可控:线程数量可空和任务可控
功能强大:可以定时和重复执行任务
二、ExecutorService简介
通常来说我们说到线程池第一时间想到的就是它:ExecutorService,它是一个接口,其实如果要从真正意义上来说,它可以叫做线程池的服务,因为它提供了众多接口api来控制线程池中的线程,而真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具有线程池的特性,其中包括工作队列、核心线程数、最大线程数等。
三、线程池中的几个参数
参数解释如下(重要):
corePoolSize:核心线程数量,不会释放。
maximumPoolSize:允许使用的最大线程池数量,非核心线程数量,闲置时会释放。
keepAliveTime:闲置线程允许的最大闲置时间。它起作用必须在一个前提下,就是当线程池中的线程数量超过了corePoolSize时,它表示多余的空闲线程的存活时间,即多余的空闲线程在超过keepAliveTime时间内没有任务的话则被销毁。而这个主要应用在缓存线程池中
unit:闲置时间的单位。
workQueue:阻塞队列,用来存储已经提交但未被执行的任务,不同的阻塞队列有不同的特性。
threadFactory:线程工厂,用来创建线程池中的线程,通常用默认的即可
handler:通常叫做拒绝策略,1、在线程池已经关闭的情况下 2、任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务 。在上面两种情况下,只要满足其中一种时,在使用execute()来提交新的任务时将会拒绝,而默认的拒绝策略是抛一个RejectedExecutionException异常
上面的参数理解起来都比较简单,不过workQueue这个任务队列却要再次说明一下,它是一个BlockingQueue<Runnable>对象,而泛型则限定它是用来存放Runnable对象的,刚刚上面讲了,不同的线程池它的任务队列实现肯定是不一样的,所以,保证不同线程池有着不同的功能的核心就是这个workQueue的实现了,细心的会发现在刚刚的用来创建线程池的工厂方法中,针对不同的线程池传入的workQueue也不一样,五种线程池分别用的是什么BlockingQueue:
1、newFixedThreadPool()—>LinkedBlockingQueue 无界的队列
2、newSingleThreadExecutor()—>LinkedBlockingQueue 无界的队列
3、newCachedThreadPool()—>SynchronousQueue 直接提交的队列
4、newScheduledThreadPool()—>DelayedWorkQueue 等待队列
5、newSingleThreadScheduledExecutor()—>DelayedWorkQueue 等待队列
四、线程池中用到的三种阻塞队列
1. LinkedBlockingQueue:无界的队列
它的容量是 Integer.MAX_VALUE,为 231 -1 ,是一个非常大的值,可以认为是无界队列。FixedThreadPool 和 SingleThreadExecutor 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。
2. SynchronousQueue:直接提交的队列
如果不希望任务在队列中等待,而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
3. DelayedWorkQueue:等待队列
它对应的线程池分别是 ScheduledThreadPool 和 SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。
DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构(堆的应用之一就是 优先级队列)。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。
2.5 线程池的种类有哪些:五种功能不一样的线程池:
这样创建线程池的话,我们需要配置一堆东西,非常麻烦。所以,官方也不推荐使用这种方法来创建线程池,而是推荐使用Executors的工厂方法来创建线程池Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池(但底层实现还是通过ThreadPoolExecutor),从而使得我们创建线程池非常的简便,主要提供了如下五种功能不一样的线程池:
newCachedThreadPool() :返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。
newFixedThreadPool() :线程池只能存放指定数量的线程池,线程不会释放,可重复利用。
newSingleThreadExecutor() :单线程的线程池。即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。
newScheduledThreadPool() :可定时和重复执行的线程池。
newSingleThreadScheduledExecutor():同上。和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小
通过Executors的工厂方法来获取
通过Executors的工厂方法来创建线程池极其简便,其实它的内部还是通过new ThreadPoolExecutor(…)的方式创建线程池的,我们看一下这些工厂方法的内部实现:
2.6 五种线程池使用举例:
1. newFixedThreadPool 创建一个固定线程数量的线程池,示例为:
创建了一个线程数为3的固定线程数量的线程池,同理该线程池支持的线程最大并发数也是3,而我模拟了10个任务让它处理,执行的情况则是首先执行前三个任务,后面7个则依次进入任务队列进行等待,执行完前三个任务后,再通过FIFO的方式从任务队列中取任务执行,直到最后任务都执行完毕。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v("zxy", "线程:"+threadName+",正在执行第" + index + "个任务");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
2. newSingleThreadExecutor
创建一个只有一个线程的线程池,每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待线程处理完再依次处理任务队列中的任务,示例为:
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
for (int i = 1; i <= 10; i++) {
final int index = i;
singleThreadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v("zxy", "线程:"+threadName+",正在执行第" + index + "个任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
其实我们通过newSingleThreadExecutor()和newFixedThreadPool()的方法发现,创建一个singleThreadExecutorPool实际上就是创建一个核心线程数和最大线程数都为1的fixedThreadPool。
3. newCachedThreadPool
创建一个可以根据实际情况调整线程池中线程的数量的线程池,为了体现该线程池可以自动根据实现情况进行线程的重用,而不是一味的创建新的线程去处理任务,我设置了每隔1s去提交一个新任务,这个新任务执行的时间也是动态变化的,示例为
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 1; i <= 10; i++) {
final int index = i;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v("zxy", "线程:" + threadName + ",正在执行第" + index + "个任务");
try {
long time = index 500;
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
4. newScheduledThreadPool
创建一个可以定时或者周期性执行任务的线程池,示例为:
5. newSingleThreadScheduledExecutor
创建一个可以定时或者周期性执行任务的线程池,该线程池的线程数为1,示例为
这个和上面的没什么太大区别,只不过是线程池内线程数量的不同,效果为:每隔2秒就会执行一次该任务
2.7 线程池的状态:
1. RUNNING:线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。
2. SHUTDOWN:不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。
3. STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。
4.TIDYING:SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为 TIDYING 状态。
线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。
- TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态。
2.8线程池的停止
关于线程池的停止,ExecutorService为我们提供了两个方法:shutdown和shutdownNow,这两个方法各有不同,可以根据实际需求方便的运用,如下:
1. shutdown() 平滑的关闭线程池。(如果还有未执行完的任务,就等待它们执行完)。
2. shutdownNow() 简单粗暴的关闭线程池。(没有执行完的任务也直接关闭)。
2.9线程池的工作流程
简单说:
- 任务来了,优先考虑核心线程。
- 核心线程满了,进入阻塞队列。
- 阻塞队列满了,考虑非核心线程。
- 非核心线程满了,再触发拒绝任务。
详细说明:
1 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于coolPoolSize,则创建一个线程执行该任务。
2 如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列。
3 如果任务不能入队,说明等待队列已满,若当前池中线程数小于maximumPoolSize,则创建一个临时线程(非核心线程)执行该任务。
4 如果当前池中线程数已经等于maximumPoolSize,此时无法执行该任务,根据拒绝执行策略处理。
注意:当池中线程数大于coolPoolSize,超过keepAliveTime时间的闲置线程会被回收掉。回收的是非核心线程,核心线程一般是不会回收的。如果设置allowCoreThreadTimeOut(true),则核心线程在闲置keepAliveTime时间后也会被回收。
任务队列是一个阻塞队列,线程执行完任务后会去队列取任务来执行,如果队列为空,线程就会阻塞,直到取到任务。