线程池的种类⭐⭐⭐⭐⭐
线程池的优点⭐⭐⭐⭐
平时当中使用案例⭐⭐⭐⭐
ThreadPoolExecutor类有了解吗?⭐⭐(需掌握,但一般不会提问)
目录
1、为何要使用线程池,有何优点
2、ThreadPoolExecutor
2.1 构造函数
2.2 线程池工作原理
3、线程池的种类和特点
3.1 FixThreadPool
3.2 SingleThreadPool
3.3 CachedThreadPool
3.4 ScheduledThreadPool
总结
4、实际使用案例
1、为何要使用线程池,有何优点
平时的安卓开发中,很多耗时操作不能在主线程中执行,不然就会阻塞主线程,因此我们常见的做法是new 一个Thread来执行耗时操作,最后通过Handler切换到主线程来修改UI。然而当线程数量多的时候,有可能会导致死机和OOM。在安卓中,我们可以使用线程池来管理我们所创建的线程。所谓线程池就是事先创建一系列线程,把它们放在一个容器里,使用的时候直接从池子里拿线程,而不需要重新去new一个。我们先讨论线程池的优点:
多个线程可以多次复用,避免因线程频繁创建和销毁给系统带来的损耗,同时也可以提高系统响应速度,当任务需要执行时不需要创建新的线程就可以执行;
线程池的最大并发数可控制,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象;
能够对线程进行管理,并提供定时执行以及定间隔循环执行等功能;
2、ThreadPoolExecutor
2.1 构造函数
待会会讲的4种线程池都是通过ThreadPoolExecutor来创建的,因此需要先了解ThreadPoolExecutor:
// 构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
// 使用demo
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1000, TimeUnit.SECONDS, new LinkedBlockingQueue<>()), new ThreadPoolExecutor.AbortPolicy());
// 通过循环向线程池中添加10个任务
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
// do something
});
}
// 关闭线程池
executor.shutdown();
先看看构造函数的各个参数:
corePoolSize:线程池的核心线程数量。核心线程当该线程处于闲置时间也不会回收的线程。当然,我们可以执行threadPoolExecutor.allowCoreThreadTimeOut(true)后,当闲置的核心线程的等待新任务时间超过了keepAliveTime也会被终止;
maximumPoolSize:线程池最大线程数,等于核心线程 + 非核心线程。如果运行的线程总数量超过这个数值,那么接下里新来的任务都会阻塞等待;
keepAliveTime:闲置的非核心线程等待时间超过keepAliveTime设定的时间就会被回收,而核心线程需要执行threadPoolExecutor.allowCoreThreadTimeOut(true)后超过时间才会被回收;
unit:keepAliveTime的单位:
单位 | 含义 |
TimeUnit.DAYS | 天 |
TimeUnit.HOURS | 时 |
TimeUnit.MINUTES | 分 |
TimeUnit.MILLISECONDS | 毫秒 |
TimeUnit.MICROSECONDS | 微秒 |
TimeUnit.NANOSECONDS | 纳秒 |
workQueue:这就是我们执行execute()添加的任务所存放的工作阻塞队列,有以下选项: 阻塞队列 | 说明 | | ----- | ----- | | ArrayBlockingQueue | 基于数组实现的有界的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。 | | LinkedBlockingQueue | 基于链表实现的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。 | | SynchronousQueue | 内部没有任何容量的阻塞队列。在它内部没有任何的缓存空间。对于SynchronousQueue中的数据元素只有当我们试着取走的时候才可能存在。 | | PriorityBlockingQueue | 具有优先级的无限阻塞队列。|
threadFactory:从名字直接翻译就知道是“线程工厂”,用于新线程的创建,默认为Executors.defaultThreadFactory();
handler:可以看到是RejectedExecutionHandler对象,该对象只是一个接口类,里面只有一个rejectedExecution方法。如果当前的活动线程达到maximumPoolSize或者执行任务失败了,就会执行rejectedExecution()。在ThreadPoolExecutor有四个内部类实现了rejectedExecution()方法,代表四种不同的处理方式:
可选值 | 说明 |
CallerRunsPolicy | 只用调用者所在线程来运行任务。 |
AbortPolicy | 直接抛出RejectedExecutionException异常。 |
DiscardPolicy | 丢弃掉该任务,不进行处理。 |
DiscardOldestPolicy | 丢弃队列里最近的一个任务,并执行当前任务。 |
2.2 线程池工作原理
线程池的工作原理涉及核心线程、非核心线程、阻塞队列等,当有新的任务加入线程池时,池内工作原理具体如下:
当正在运行的线程数 < 核心线程数,马上创建核心线程处理这个任务;
当正在运行的线程数 >= 核心线程数,把该任务加入阻塞队列;
当阻塞队列满了且正在运行的线程数 < 线程池最大线程数,则创建新的非核心线程来处理该任务;
当阻塞队列满了且正在运行的线程数 >= 线程池最大线程数,则线程池调用handler的reject方法拒绝本次新任务添加。
3、线程池的种类和特点
Android中的线程池都是直接或间接通过配置ThreadPoolExecutor来实现不同特性的线程池。在Android中最常见的4种线程池分别为FixThreadPool、CachedhreadPool、SingleThreadPool、ScheduleThreadExecutr.
3.1 FixThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
线程数量固定的线程池,只有核心线程(因为corePoolSize = maximumPoolSize),如果线程池的线程处于空闲状态的话,这些核心线程也不会被回收,所以FixThreadPool也可以更快速地响应外界请求;
如果所有线程都在忙碌状态,如果有新的任务到来就会处于等待状态,而不会创建新的线程来执行;
适用于很稳定、很正规的并发线程,多用于服务器;
3.2 SingleThreadPool
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
单例线程池,任意时间内池中只有一个线程。因此,当有一个线程正在执行时,其他的任务都需要在任务队列里等待。
3.3 CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
无界可自动回收线程池,其中无界体现在maximumPoolSize设置为Integer.MAX_VALUE,Integer.MAX_VALUE代表很大的数,因此可以理解为无界限。可回收是因为核心线程数设置为0,超时时间设置为60秒,因此该线程池里所有的线程一旦处于闲置时间超过60秒就会被自动回收了。因此这种线程池有以下特点:
任何任务立即被执行;
闲置时不占系统资源;
适合执行大量的耗时较少的任务;
3.4 ScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
周期任务线程池,核心线程数固定,非核心线程则没有限制数量,非核心线程闲置时立刻回收。适合执行定时任务以及有固定周期的重复任务。
总结:
类型 | 创建方法 | 说明 |
FixedThreadPool | Executors.newFixedThreadPool(int nThreads) | 线程数固定的线程池,只有核心线程,并且不会被回收,没有超时机制 |
CachedThreadPool | Executors.newCachedThreadPool() | 无界可自动回收线程池,只有非核心线程,闲置线程超过60秒自动回收 |
ScheduledThreadPool | Executors.newScheduledThreadPool(int corePoolSize) | 周期任务线程池,核心线程数固定,非核心线程数无限制,非核心线程闲置时立刻回收 |
SingleThreadExecutor | Executors.newSingleThreadExecutor() | 单例线程池,确保所有任务在同一线程中按顺序执行 |
4、实际使用案例
面试官问完理论题目后,还会问实际开发中使用线程池的实际案例。以下给一个之前在做EMMC测试时需要写文件的实际demo:
mXRExecutorService = Executors.newFixedThreadPool(THREAD_NUMBS);
private void writeFileWithMultiThread(final File file, int size, byte[] buff){
if (file == null) {
return;
}
// CountDownLatch允许一个或者多个线程去等待其他线程完成操作
CountDownLatch connectedSignal = new CountDownLatch(THREAD_NUMBS);
long sizeOfPerThread = size / THREAD_NUMBS;
for (int i = 0; i < THREAD_NUMBS; ++i) {
byte[] perByte = Arrays.copyOfRange(buff, (int)(i * sizeOfPerThread), (int)((i + 1) * sizeOfPerThread));
writeFile(file, connectedSignal, perByte, (int)(i * sizeOfPerThread));
}
waitForLatch(connectedSignal); // 自定义函数,实际调用await()
}
private void writeFile(final File file, final CountDownLatch connectedSignal, byte[] byteValue, int startIndex) {
mXRExecutorService.execute(() -> {
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(file, "rwd");
raf.seek(startIndex);
raf.write(byteValue);
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
LogUtils.d("write index=" + startIndex + " over");
connectedSignal.countDown();
});
}