注册

代码优化一下,用线程池管理那些随意创建出来的线程

线程大家一定都用过,项目当中一些比较耗时的操作,比如网络请求,IO操作,我们都会把这类操作放在子线程中进行,因为如果放在主线程中,就会多少造成一些页面卡顿,影响性能,不过是不是放在子线程中就好了呢,我们看看下面这段代码


image.png

很简单的一段代码,创建了一个Thread,然后把耗时工作放在里面进行就好了,如果项目当中只有一两处出现这样的代码,倒也影响不大,但是现在的项目当中,耗时的操作一大堆,比如文件读取,数据库的读取,sp操作,或者需要频繁从某个服务器获取数据显示在屏幕上,比如k线图等,像这些操作如果我们都去通过创建新的线程去执行它们,那么对性能以及内存的开销是很大的,所以我们在平时开发过程当中应该养成习惯,不要去创建新的线程而是通过使用线程池去执行自己的任务


线程池


为什么要使用线程池呢?线程池总结一下有以下几点优势



  • 降低资源消耗:通过复用之前创建过的线程资源,降低线程创建与销毁带来的性能与内存的开销
  • 提高响应速度:无需等待线程创建,直接可以执行任务
  • 提高线程可管理性:使用线程池可以对线程资源统一调优,分配,管理
  • 使用更多扩展功能:使用线程池可以进行一些延迟或者周期性工作

而我们创建线程池的方式有以下几种



  • Executors.newFixedThreadPool:创建一个固定大小的线程池
  • Executors.newCachedThreadPool:创建一个可缓存的线程池
  • Executors.newSingleThreadExecutor:创建单个线程数的线程池
  • Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池
  • Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池
  • Executors.newWorkStealingPool:创建一个抢占式执行的线程池
  • ThreadPoolExecutor:最原始的创建方式,以上六种方式的内部也是通过这个创建的线程池

虽然我们提倡使用线程池,但是有这么多的创建方式,我们如果不在项目当中做一下管理的话,那么各种各样的线程池都有可能被使用到,由于每种创建方式对于线程的管理方式都不一样,如果不合理创建的话,很可能会出现问题,所以我们需要有一个统一创建线程池的地方


统一管理线程


image.png

首先我们先创建一个线程池,使用Executors.newCachedThreadPool()去创建一个
ExecutorService,至于为什么选择newCachedThreadPool(),我们看下它的源码


image.png

从上面一大段英文注释中我们能知道,这是一个可缓存的线程池,并且corePoolSize为0说明这个线程池没有始终存活的线程,如果线程池中没有可用线程,会重新创建新线程,而线程池中如果有可用线程,那么这个线程会被再利用,一个线程如果60秒内没有被使用,那么将会从队列中移除并销毁,所以个人感觉对于并发要求不是特别高的移动端,从性能角度来讲使用这样的一个线程池是比较合适的,当然具体设计方案以业务性质来决定,现在我们可以将项目当中的线程放在我们的线程池里面运行了,再增加一个执行线程的函数


image.png

通过这个函数就可以有效的避免项目当中随意创建线程的现象发生,让项目当中的线程可以井然有序的运行,但是这还没完事,我们知道Runnable在任务执行完成之后是没有返回结果的,因为Runnable接口中的run方法的返回类型是个void,但实际开发当中,我们的确有需求,在执行一些比如查询数据库,读取文件之类的操作中,需要获取任务的执行结果,之前都是通过在线程当中手动添加一个handler将需要的数据传递出来,再专业一点使用RxJava或者Flow,但不管什么方式,这些都会造成代码耦合,我们还有更简单的方式


Callable和Future


这两个类是在java1.5之后推出来的,目的就是解决线程执行完成之后没有返回结果的问题,我们先来对比下Runnable与Callable


image.png

相比较于Runnable,Callable接口里面也有一个call的方法,这个方法是有返回值的,并且可以抛出异常,所以以后当我们需要获取任务的执行结果的时候,我们还可以使用Callable代替Runnable,那么如何使用并获取返回值呢?当然是使用我们已经创建好的ExecutorService,它里面提供了一个函数去执行Callable


image.png

使用submit函数就可以执行我们的Callable,返回值是一个Future,而如何去获取真正的返回结果,就在Future里面,我们看下


image.png

使用get方法就可以获取线程的执行结果,我们现在就来试试Callable和Future,在PoolManager里面再增加一个函数,用来执行Callable


image.png

我们这里有个简单的任务,就是返回一段文字,然后将这段文字显示在界面上,那么第一步,先在布局文件里面添加一个按钮


image.png

然后点击这个按钮,将任务里面返回的文字显示在按钮上,代码如下


image.png

得到的效果如下


aa2.gif


在这边特地把执行结果放在界面上而不是用日志打印出来的原因可能大家已经发现了,Callable在返回执行结果的同时,也帮我们把线程切回到了主线程,所以我们不用在特地去切换线程更新ui界面了


周期性任务


普通的单个任务我们讲完了,但是在项目当中往往会存在一些比较特殊的任务,可能需要你去周期性的去执行,举个常见的例子,在证券类的app里绘制k线图的时候,并不需要将服务器吐出来的数据统统拿出来绘制ui,这样对性能的开销是很大的,我们正确的做法是将数据都先存放在一个buffer里面,然后定时的去buffer里面拿最新数据就好,那这样一个定时刷新的功能如何在我们的线程池里面去实现呢,这里就要用到刚刚说到的另一种创建线程池的方式


image.png

这个函数创建的是一个ScheduledExecutorService对象,可周期性的执行任务,入参的corepoolSize表示可并发的线程数,现在我们在PoolManager里面添加上这个ScheduledExecutorService


image.png

而如何去执行任务,我们使用ScheduledExecutorService里面的scheduleAtFixedRate函数,我们先看下这个函数都有哪些入参


image.png

不用去看注释我们就能知道怎么使用这个函数,command就是执行的任务,第二,第三个参数分别表示延迟执行的时间以及任务执行的周期时间,第四个参数是时间的单位,在看返回值是一个ScheduleFuture,既然也是个Future,那是不是也可以通过它去获取任务执行的结果呢?答案是拿不到的,一个原因是command是一个Runnable而不是Callable,不会返回任务的执行结果,另外我们从注释上就能了解,这个ScheduleFuture只是用来当周期任务中有任务被取消了,或者被异常终止的时候,抛出异常用的,那ScheduledExecutorService一定有入参是Callable的函数的吧,找了找发现并没有,那只有一个办法了,我们在command里面去执行一个Callable任务,再将任务的执行结果回调出来就好了,代码设计如下


image.png

我们创建了一个函数叫executeScheduledJob,也有四个入参,job是一个Callable,用来执行我们的任务,callback是一个回调,用来将任务执行结果回调到上层去处理,后面两个刚刚已经介绍过了,这里设置了默认值,可自定义,现在我们就来实现一个简单的读秒功能,点击刚刚那个按钮,按钮初始值是1,然后每秒钟加一,代码实现如下


image.png

这边创建了一个CounterViewModel用来执行计数器的逻辑,dataState是一个StateFlow并且设置了初始值1,在onCallback里面接收到了任务执行结果并发送至上层展示,上层的代码逻辑如下


image.png

现在这个计时器功能完成了,我们来执行下代码看看效果如何


aa3.gif


我们这边使用StateFlow发送数据还有个好处,当接收的数据中有些数据需要过滤掉的时候,我们还可以使用StateFlow提供的操作符实现,比如这边我们只想展示奇数,那么代码可以改成如下所示


image.png

使用filter操作符将偶数的值过滤掉了,我们再看看效果


aa4.gif


总结


我们的这个线程管理工具到这里已经完成了,不是很复杂,但是项目当中存不存在这样一个工具明显会对整体开发效率,代码的可读性,维护成本,以及一个app的性能角度来讲都会有个很大的提升与改善,后面如果还做了其他优化工作,也会拿出来分享。


作者:Coffeeee
链接:https://juejin.cn/post/7215185077444345917
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册