跳到主要内容

多线程

多线程可以让你同时异步执行多种任务,是各种编程语言里很重要的一个概念。合理的采用多线程可以让你的 App 拥有更好的运行性能,但是如果使用不当可能会让你的程序非常混乱,出现很多令人费解且难以定位的问题。

1. 多线程初探

当用户打开一个 App 时,Android 系统会创建一个 Linux 进程,同时在进程中创建一个执行线程,我们称之为“主线程”,因为 Andfoid 规定只能在主线程更新 UI,所以又叫“UI线程”。

系统在创建主线程的时候帮我们创建好了一套消息处理机制,包含了第 38 节提到的 Handler、Looper、MessageQueue 等模块,主线程就利用这一套消息机制来实现 Actvity、Fragment 的生命周期回调以及和其他 App 之间的通信。所有需要在 UI 线程执行的任务都要首先被 push 到任务队列中,然后等待主线程 Looper 来轮询。如果我们将所有的任务都放到主线程的任务队列,那么可能需要等很久才能执行到,所以一个比较好的选择就是将耗时任务单独放到一个子线程中,这样就可以独享一个 MessageQuene,并且不再占用主线程的资源。

2. 多线程注意事项

  • Android 规定刷新 UI 的操作必须在主线程执行;
  • 网络请求、数据库或者文件 I / O 等都属于耗时操作,非常容易导致主线程的阻塞造成 App 卡顿;
  • 由于主线程的 Looper 是按顺序轮询 MessageQueue 的,所以主线程的所有任务都是同步执行。这样如果有耗时操作那么会阻塞主线程,后面的任务都需要等待耗时操作的执行;
  • 除了 I / O 操作外,开发人员需要自行评估任务的耗时情况,合理采用多线程避免主线程的阻塞;
  • Android 提供了多种创建和管理线程的方法,当然如果有高并发的场景还有一些第三方库可以使用,但是系统的线程、线程池可以应对大部分常见场景。

接下来我们来看看具体怎么使用 Android 多线程。

3. 线程的使用方法

Java 虚拟机支持多线程并发编程,并发意味着同时执行多个任务。在 Android 中常见的多线程常见就是在子线程执行耗时操作,然后将结果通过线程间通信传递给主线程,主线程仅仅拿到结果进行 UI 的刷新。

3.1 线程的创建

我们有两种方式进行线程的创建

  1. 继承Thread实现一个线程类:
class TestThread extends Thread {
@Override
public void run() {
Log.d("Threading", "继承 Thread 的线程:"+Thread.currentThread().getName());
}
}

  1. 实现Runnable接口
class TestRunnable implements Runnable {
@Override
public void run() {
Log.d("Runable", "实现 Runable 的线程>"+Thread.currentThread().getName());
}
}

无论是哪种方式,都需要在类中实现一个无参的run()方法,然后将线程的实际执行任务放在run()方法中,在要用多线程的类中需要创建出一个Runnable接口实例。

3.2 启动进程

对于第一种创建方式,直接创建 TestThread 实例调用start()即可:

new TestThread().start()

而对于第二种方式,在创建 Thread 的同时传入 Runnable 接口实例,然后调用start()

new Thread(new TestRunnable()).start()

调用了 Thread 对象的 start()之后,run()方法就会在我们的子线程中执行了。

3.3 线程生命周期

和 Activity 一样,Thread 在执行过程中也有自己的生命周期,一共有 5 种状态:

  • **New:**刚创建好,还未执行
  • **Runnable:**已经调用了start(),等待 CPU 分配时间片
  • **Running:**正在运行
  • **Blocked:**由于某些原因(等待、睡眠、CPU暂时回收资源等)线程进入阻塞
  • **Dead:**线程任务执行结束,或者主动关闭

各个生命周期的切换如下图:

thread

4. 多线程示例

本节创建两个耗时子线程,在线程的开始和结束分别打上日志,然后观察两个线程任务是同时执行,还是需要等待其中一个线程的耗时任务执行结束才能执行第二个。

MainActivity 代码很简单,在里面创建两个线程,为了方便演示我们用“继承自 Thread”和“实现 Runnable”两种方式来创建两个线程:


package com.emercy.myapplication;

import android.app.Activity;
import android.content.pm.PackageManager;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.annotation.Nullable;

import java.io.IOException;
import java.util.Random;

import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;

public class MainActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

new Thread() {
@Override
public void run() {
Log.d("ThreadTest", "Thread1 start");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("ThreadTest", "Thread1 end");

}
}.start();

new Thread(new Runnable() {
@Override
public void run() {
Log.d("ThreadTest", "Thread2 start");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("ThreadTest", "Thread2 end");
}
}).start();
}

private void task() {
for (int i = 0; i < 10; i++) {
Log.d("Thread", Thread.currentThread().getName() + " 当前i = " + i);
}
}
}

在两个线程中通过sleep()来模拟 500 毫秒的耗时任务,在任务的开始和结束都打上日志,观察结果如下:

图片描述

可以看到首先会同时开启两个子线程,然后分别同时执行 500 毫秒的任务,在执行结束打上结束的 Log,可以证明两个 Thread 是同时执行的。

5. 小结

本节学习了一个能让你的 App 并发高效执行任务的方式,多线程可以帮助你提升 App 的整体性能,但用之不当可能会造成一定的资源浪费,所以一定要谨记本节所提到的注意事项。然后按照步骤去创建、运行子线程,了解线程执行的生命周期,让程序更好的为用户服务。