评论

收藏

[Android] Android高工:细说 Android 多线程,探究原理知其所以然

移动开发 移动开发 发布于:2021-12-17 22:57 | 阅读数:396 | 评论:0

本篇文章仅介绍多线程的各种实现方式,不过多涉及深入的基础原理探究,达到“所见即所学,所学即可用”的效果。关于各种多线程原理的深入探究,有机会放在后面逐一介绍。
[]( )一、多线程是什么?我为什么要用多线程?
===============================================================================
1.1 线程和进程的概念
按照操作系统中的描述,线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程。
简单点理解,一个Android APP就是一个进程,一个APP里面有多个线程,我们多线程编程的意义就是实现“一个APP多个线程”。
有杠精可能会问,那我可不可以一个APP多个进程?又可不可以一个进程只有一个线程?
我告诉你,可以,都可以。
单线程的APP只包括Android的UI线程也是能运行的;一个APP多个进程也是可以达到的,实现方式涉及到Android的IPC机制,这里不细说。
1.2 为什么要使用多线程?
这里杠精可能会说,那你单线程也能跑,我为啥还要整多线程?
我告诉你,首先这句话从Android开发的角度来讲,近似于一个假命题。因为谷歌爸爸现在强制规定了不能在UI线程进行耗时操作,必须放到子线程里面去,除非你的程序不涉及耗时操作。究其原因,是因为在UI线程进行耗时操作的话,给用户的使用体验就是界面“卡顿”。同时,如果UI线程被阻塞超过一定时间会触发ANR(Application Not Responding)错误。
从底层的角度来讲,多线程可以使得整个环境能够异步执行,这有助于防止浪费CPU时钟周期从而提高效率。换言之,多线程能更充分的利用CPU资源,从而提高程序的运行效率。
[]( )二、那我怎么进行多线程编程?
==========================================================================
2.1 Thread类和Runnable接口 要想定义一个线程只需要新建一个类继承自Thread,然后重写父类的run方法即可
class MyThread extends Thread {
@Override
public void run() {
doSomething();
}
}
//在需要的时候启动线程
new MyThread().start();
优化一下?
我们可以没必要继承整个Thread类,只实现Runnable接口就好了
class MyThread implements Runnable {
@Override
public void run() {
doSomething()
}
}
//启动线程
MyThread myThread = new MyThread();
new Thread(myThread).start();
那我不想专门再写一个线程类怎么办?可以使用匿名类
new Thread(new Runnable() {
@Override
public void run() {
doSomething();
}
}).start();
2.2 线程池

  • 2.2.1 线程池的意义
既然我都会用Runnable接口来创建线程了,还要线程池干啥?其实不然,随意创建线程的操作在实际开发中是极为不推荐的。为啥?因为线程也是一种资源,反复的创建和销毁线程会带来一定性能上的额外开销。与其相比,线程池主要有以下几个优点:

  • 重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销
  • 能有效控制线程池的最大并发数,避免大量的线程之间因相互抢占系统资源而导致的阻塞现象
  • 能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能
  • 2.2.2 线程池的结构和原理
一个完整的线程池应该有这么几个组成部分

  • 核心线程
  • 任务队列
  • 非核心线程
当我们通过线程池执行异步任务的时候,其实是依次进行了下面的流程

  • 检查核心线程数是否到达最大值,否则创建新的核心线程执行任务,是则进行下一步
  • 检查任务队列是否已满,否则将任务添加到任务队列中,是则进行下一步
  • 检查非核心线程数是否到达最大值,否则创建新的非核心线程执行任务,是则说明这个线程池已经饱和了,执行饱和策略。默认的饱和策略是抛出RejectedExecutionException异常
下面手搓一个线程池的实现
//CPU核心数
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//核心线程数
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
//最大线程数
private static final int MAX_POOL_SIZE = CPU_COUNT 2 + 1;
//非核心线程闲置的超时时间
private static final int KEEP_ALIVE_TIME = 1;
//任务队列
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
//线程池
private ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, sPoolWorkQueue);
private void fun(){
Runnable runnable = new Runnable() {
@Override
public void run() {
//子线程处理耗时操作
doSomething();
}
};
poolExecutor.execute(runnable);
}
这样我们就实现了一个简单的线程池,核心线程数为CPU数量+1,非核心线程数为CPU数量
2+1,非核心线程的闲置时间为1秒,任务队列的
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整资料开源分享
大小为128。
线程池还有具体的好几种分类和相应不同的实现方式,这里不再细说。
2.3 Handler
有朋友可能会说,你讲的这些都是Java多线程里面的东西,能不能整点咱Android特有的?OK,现在进入专业时间。
Handler是Android提供的一种异步消息处理机制,要学会使用Handler我们首先来了解下消息处理四兄弟:

  • Message
  • Handler
  • MessageQueue
  • Looper
Handler可以帮助我们实现在不同的线程之间传递消息,这里的Message就是消息本体,也就是我们想要传递的那个东西。
Handler在这里扮演的角色是消息处理者,它的主要作用是发送和处理消息。
MessageQueue是一个消息队列,Handler发送过来的消息会放在这个队列里面,每个线程只会有一个MessageQueue对象。
Looper是线程中消息队列的管家,它会无限循环运行,每发现MessageQueue中存在一条消息,它就会把消息取出然后发送给Handler。每一个线程也只能有一个Looper对象。
好了,基本原理已经了解,现在我们来反手搓一个Handler
private static final int FLAG = 1;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
if (FLAG == msg.what){
//这里已经回到主线程了
doSomething();
}
}
};
private void fun(){
new Thread(new Runnable() {
@Override
public void run() {
//子线程发送消息
Message message = new Message();
message.what = FLAG;
mHandler.sendMessage(message);
}
}).start();
}
2.4 AsyncTask
除了Handler以外,谷歌爸爸还给我们提供AsyncTask来进行线程的切换。AsyncTask是一种轻量级的异步任务,它可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程。从实现原理上来讲,AsyncTask是对Thread和Handle的再次封装。
AsyncTask本身是一个抽象的泛型类,有四个亲儿子:

  • onPreExecute()
  • doInBackground(Params…params)
  • onProgressUpdate(Progress…values)
  • onPostExecute(Result result)
最先执行的是方法是onPreExecute()方法,位于主线程中,一般用来做一些准备工作。
然后执行doInBackground()方法,位于线程池中,用来执行异步任务,params表示异步任务的输入参数。这个方法需要返回结果给onPostExecute()方法。
onProgressUpdate()方法在主线程中执行,当后台任务的执行进度发生变化时这个方法会被调用。
onPostExecute()方法在最后异步任务完成之后会被调用,位于主线程中,result参数是后台任务的返回值,即doInBackground()的返回值。
OK,基本原理已经了解了,现在我们来手搓一个AsyncTask
class DownloadTask extends AsyncTask<Void,Integer,Boolean> {
@Override
protected void onPreExecute() {
//这里我们使用了一个显示进度的Dialog,具体实现不表
progressDialog.show();
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
while (true){
//调用我们的doDownload下载方法,具体实现不表
int downloadPercent = doDownload();
//使用publishProgress方法来更新执行的进度
publishProgress(downloadPercent);
if (downloadPercent >= 100)
break;
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
@Override
protected void onProgressUpdate(Integer... values) {
//更新下载进度
progressDialog.setMessage("Download "+values[0]+"%");
}
结尾

  • 腾讯T4级别Android架构技术脑图;查漏补缺,体系化深入学习提升
DSC0000.jpg


  • 一线互联网Android面试题含详解(初级到高级专题)
这些题目是今年群友去腾讯、百度、小米、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。并且大多数都整理了答案,熟悉这些知识点会大大增加通过前两轮技术面试的几率
DSC0001.jpg

有Android开发3-5年基础,希望突破瓶颈,成为架构师的小伙伴,可以关注我
本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录
</div>
    
    <div id="asideoffset"></div>

关注下面的标签,发现更多相似文章