前言
多线程相关的基础,相信大家都知道,但是在真实的开发中使用到多线程,确实少之又少。但其实多线程开发大有用处,只是很多时候我们都是不得已才去使用,而不是会主动思考某个功能是否要使用多线程去实现。在线开发也稍微有点年限,这里可以浅谈一下我对实战中使用多线程的理解。
1. 使用风险
讲使用之前,还是必须要强调一下风险,这也是我们学这块基础知识的时候或者说一些面试,都会要强调的。因为多线程的环境中会存在一些风险,要在实战中更好的使用多线程,那就必须了解他可能会导致的问题。
举个例子,有时候应用中会出现一些奇怪的现象,比如我明明改了这个对象,但是却没改成功,我明明做了更新,但最终的表现还是更新前的现象等等。多线程的问题会导致一些奇怪的现象产生,而这些现象基本的共同点就是非必现、难排查。有经验的朋友应该都知道,这类现场相关的问题是真的很难排查,必须要花费一些精力才能处理。通俗来讲就是它不难,但是它能让你掉一层皮。
所以多线程相关的基础知识很重要,原子性等3个特性、加锁、各种锁、休眠等基础的知识点一定要都理解了再来使用,这篇文章我就不总结这些基础知识了。
总结来说就是,在使用的时候,一定要认真思考此处使用多线程会不会在某种情况下导致某些异常现象,这是其一,其二就是更深层次的思考,以后的有人来修改、扩展这块功能,这里的多线程是否会对其造成某些影响。
2. 和流程无关联的步骤可以使用多线程
这是一个比较万金油的场景,假设我把当前的页面的业务逻辑细分为很多个任务,task1 ... taskn,我的task2要拿task1的结果才能进行,但是task3不需要任何前置的操作,而且当前应用的所有任务,不需要拿task3的结果,那这个task3就是一个和流程无关的操作,如果它是一个比较耗时的操作,那就可以用多线程去实现。
举个例子,我以前有碰到过这样的需求,应用进入首页后,要上传日志,而这个日志又要做一些截取关键字相关内容,不上报全部。我们都知道,android的日志会有很多内容,所以遍历它会是一个耗时的操作。但是这里的任务只是遍历截取上报日志文件,和其它的任务、步骤没有任何关联。日志能直接从设备文中拿,最终上传后就结束了。上传的流程肯定是异步在其他线程进行,但是默认的情况下,遍历和截取内容的操作是在主线程进行的,而这又是一个耗时操作,所以可以开一条线程去做。
ok,然后这事还没完,我们还得考虑风险是不是,那上面那个例子的风险是什么,如果上传过程中,我退出页面再重进,又传一次,这肯定不行,我肯定只需要一份日志就行,重复传多次对我来说只是浪费资源,所以我要用一个状态来表示整个流程,如果正在上传就不重复传,那这个状态就会涉及到多线程的问题,你就得想办法保证这个状态的可见性。
3. 复杂的计算可以考虑放在子线程中进行
这种情况一般操作都是开一条线程做耗时的计算操作,然后再把计算的结果给到主线程,怎么给?通过handler啊,不然面试官老考你handler干嘛。
这个复杂的计算一般场景都不会碰到,如果你的数据源有很大的数据,然后你的结果是根据这个庞大的数据源去计算出来的,比较经典的就是O(n^m)这种情况,计算这个数据套了很多层for
或者比较经典的就是绘制复杂的矢量图,因为你要根据数据去计算出各个点位,而复杂的矢量图往往需要比较多的点位,比如说你做股票软件的,你的折线图就是要有很多点位,这些点位是根据后台返回的数据去算出来的,算他们的x\y坐标 (当然如果你的股票每天都跌停那就不用算了【狗头】)
假设你的计算全部放在主线程做,假如在onDraw方法中做,我们可以简单看看onDraw的线程
可以看到onDraw里面的操作是在主线程中的,你在主线程做耗时计算,你不卡谁卡。那要怎么改,大致可以这样(这里只是写个思路)
那这个情况下有什么风险,首先这个状态,如果你在子线程去改的话,就会有多线程问题,如果用handler发消息给主线程去改的话就不会有问题。其次这个根据源数据去计算的点位数组,如果在子线程计算中,主线程又setData设置了新的源数据,可以让线程中断,重新计算。
4. 拿多个无关任务的结果
举个例子,我有3个请求,这3个请求毫无相关,但是我最终要拿着3个请求的结果一起做操作。
这种情况下就应该开线程去做,我敢说大部分人都是做成线性的,而不是并行的。
比如3个毫无相关的任务,我们只是最终要合并他们的结果,那明明可以这样做
但是有很多人都是在实战中这样做
无非就是觉得线性的操作方便,并行的操作麻烦而且有风险。但是如果这3个task是网络请求,你要知道网络请求可是耗时操作哦,你可能觉得但是你测试的时候线性操作也很快,但是你是不是要考虑网络慢的情况。
像这种情况你使用countdownlatch、handler等方法都是能做到并行的效果。
至于风险,这个情况下使用多线程当然是有风险,但我认为这也是一个比较经典的情况,这种情况,你不能因为觉得有风险,就完全放弃使用多线程,而是要用其他办法去防止风险的发生。
5. 初始化、预加载
一个比较经典的情况,你进首页卡顿,进应用首页慢,其中一个原因就是你在首页的onCreate方法中做了大量的初始化操作。
那么你是否有考虑过把初始化的操作,放到子线程进行。你也许考虑过,但是你担心,你担心使用的时候还没初始化完成。
其实这种情况下也有很多种做法,首先你要处理进首页慢的问题,那自然是得开线程去做了,所以我们要处理的问题是在开线程去做初始化的基础上怎么去防止说我要用到这个功能但是还没初始化的情况。
这个初始化,也是能分不同的类型的,比如我对设备信息初始化,进首页后我要获取设备的信息,而且这些设备的信息是很多地方都要用到的,比如所有的请求都要传设备ID等,那这个设备信息的初始化就很重要,我就可以把它放在主线程中进行,不要开子线程去做,不然用到的地方太多了,都去适配很麻烦。
但是有些初始化操作,比如一些第三方SDK,比如你接了支付的SDK,微信支付宝,你只有在打开支付页面的时候才会用到,那这个地方的初始化完全可以放到子线程去做,至于你担心的使用到的时候还没初始化完成。比如你担心都打开支付页面了,支付SDK的初始化还没完成(我算你手快的情况)。那也完全可以处理,你可以用一个状态表示当前是否已经初始化了,然后对行为做lazy,如果已经初始化了直接进行,如果还没初始化,则等初始化之后再做lazy操作。
我这里可以写一段代码,大概就是这个意思,主要是一个思路
预加载也同理,如果一些大资源加载慢,也可以通过开条线程先去做预加载。
6. 总结
这里列举了一些比较常见的在开发中可以使用多线程进行开发的场景。
其实很多地方用多线程进行处理的话,更为合理,只是很多时候有些人会碍于多线程的风险或者代码的复杂度,所以还是选择了全部放在主线程去做。
对于代码的复杂度,我只能说写多了就习惯了,而且你得去写这些东西才能有进步,你想对这块领域有更深入的了解,就要去写。至于风险,我认为并不是就一味的觉得有风险就不去做,比如风险太大,太复杂,ok,我可以先不做,但是风险没这么大,思路又比较清晰的情况,我们不是不去做,而是思考怎么去解决这些风险。
所以面试有时候会问,你开发有没有涉及到多线程的情况,你看我上面举的例子,我感觉基本每个项目都会涉及到,只是你不敢去朝这个方向去写。如果你回答没涉及过,那我要么就觉得你不太行,要么就觉得你的项目太小,太简单,无论是哪种情况,面试官对你肯定要减分。
整篇文章说的"开一条线程",指的是用多线程去做这件事,而不是指直接new Thread,因为开线程的方式很多,java为什么直接提供给你HandlerThread,为什么给你提供CachedThreadPool、FixedThreadPool、ScheduledThreadPool、SingleThreadPool 四种线程池,都是有原因的。