Android 线程

简介

线程基础

什么是进程

进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统进行资源分配和调度的基本单元。进程可以被看作程序的实体,同样,它也是线程的容器。(Java 程序运行在 JVM 中,JVM 进程其实就是他们的容器。)

简单来讲,进程就是程序的实体,是受操作系统管理的基本运行单元。并且进程是重量级的,进程之间也是隔离的。

Android 基于 Linux,App 运行在沙箱机制,就是一个 App 独立运行在一个虚拟机中,即使出错也不会影响系统,所以每一个虚拟机,运行的过程叫做进程。

什么是线程

在操作系统中,程序里面运行的子任务就是线程,线程是 CPU 调度的最小单元,也叫作轻量级进程,同时它又是一种受限的系统资源(不可能无限制地产生,是有限的),并且线程的创建和销毁都会有相应的开销。

在一个进程中可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

当系统中存在大量的线程时,系统会通过时间片轮转的方式调度每个线程,因此线程不可能做到绝对的并行,除非线程数量小于等于 CPU 的核心数,一般来说这不可能。

线程优化:采用线程池,避免程序中存在大量的 Thread。

进程与线程的区别

进程是拥有资源的最小单位:word打开文件,QQ音乐打开了Socket。

线程是CPU调度的最小单位:T1线程接受文字输入,T2负责自动保存。T3线程负责从Socket读取数据,T4线程对音乐数据解码。

操作系统在做调度的时候基本单位不是word和QQ音乐这样的进程,而是T1、T2这种线程。

进程一般指一个执行单元,在 PC 和移动设备上指一个程序或者一个应用。它可包含多个线程。

线程,就是程序代码的执行,一个进程至少得有一个线程。每个线程执行的都是进程代码的某个片段。以word为例,如有定时保存文档的功能,当只有一个线程时,当这功能运行时,则无法输入文字。

为什么要使用多线程

在操作系统级别上来看主要有以下几个方面:

  • 使用多线程可以减少程序的响应时间。如果某个操作很耗时,或者陷入长时间的等待,此时程序将不会响应鼠标和键盘等的操作,使用多线程后可以把耗时的操作分配到一个单独的线程中去执行,从而使程序具备了更好的交互性。
  • 与进程相比,线程创建和切换开销更小,同时多线程在数据共享方面效率非常高。
  • 多 CPU 或者多核计算机本身就具备执行多线程的能力。如果使用单个线程,将无法重复利用计算机资源,这会造成资源的巨大浪费。在多 CPU 计算机中使用多线程能提高 CPU 的利用率。所以说,使用多线程不一定是为了提高效率,也许是为了发挥多核的优势,或者是更好的利用CPU快速的运算能力。
  • 使用多线程能简化程序的结构,使程序便于理解和维护。

线程的状态

Java 线程在运行的生命周期中可能会处于 6 种不同的状态:

  • New:新创建状态。线程被创建,还没有调用 start 方法,在线程运行之前还有一些基础工作做要做。
  • Runnable:可运行状态。调用 start 方法后所处于的状态。此时可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
  • Blocked:阻塞状态。表示线程被锁阻塞,它暂时不活动。(当线程调用到同步方法时,如果线程没有获得锁则进入阻塞状态,当阻塞状态的线程获取到锁时则重新回到运行状态。)
  • Waiting:等待状态。线程暂时不活动,并且不运行任何代码,这消耗最少的资源,直到线程调度器重新激活它。(需要等待其它线程通知才能返回运行状态)
  • Timed Waiting:超时等待状态。与等待状态不同的是,它可以在指定的时间自行返回。(相当于在等待状态加上了时间限制,如果超过了时间限制,则线程返回运行状态)
  • Terminated:终止状态。表示当前线程已经执行完毕。导致线程终止有两种情况。
    • run 方法执行完毕正常退出。
    • 因为一个没有捕获的异常而终止了 run 方法。

理解中断

一个线程可通过 interrupt 方法来请求中断线程,此时线程的中断标识位将被置位(中断标识位为 true),线程会不时地检测这个中断标识位,以判断线程是否应该被中断。

1
2
3
4
// 线程是否被置位
while (!Thread.currentThread().isInterrupted()){
// do something
}

也可以调用 Thread.interrupted() 来对中断标识位进行复位。但是如果一个线程被阻塞,就无法检测中断状态。因为处于阻塞状态时,如果线程在检查中断标识位时发现为 true,则会在阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常前将线程的中断标识位复位,即重新设置为 false。

需要注意的是,被中断的线程不一定会终止,中断线程是为了引起线程的注意,被中断的线程可以决定如何去响应中断。如果是比较重要的线程则不会理会中断,而大部分情况则是线程会将中断作为一个终止的请求。

另外,不要在底层代码里捕获到 InterruptedException 异常后不做处理。

1
2
3
4
5
6
7
8
9
void myTask(){
...
try{
Thread.sleep(50);
}catch (InterruptedException e){

}
...
}

下面介绍两种合理的处理方式。

1、在 catch 子句中,调用 Thread.currentThread().interrupted() 来设置中断状态(因为抛出异常后中断标识位会复位),让外界通过判断 Thread.currentThread().isInterrupted() 来决定是否终止线程还是继续下去。

1
2
3
4
5
6
7
8
9
void myTask(){
...
try{
Thread.sleep(50);
}catch (InterruptedException e){
Thread.currentThread().interrupted();
}
...
}

2、更好的做法是,不使用 try catch 来捕获这样的异常,让方法直接抛出,这样调用者可以捕获这个异常。

1
2
3
void myTask() throws InterruptedException {
Thread.sleep(50);
}

安全地终止线程

1、使用中断来终止线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class StopThread {

public static void main(String[] args) throws InterruptedException {
MoonRunner moonRunner = new MoonRunner();
Thread thread = new Thread(moonRunner,"MoonThread");
thread.start();
// 调用 sleep 方法使得 main 线程睡眠 10 ms,这是为了留给 MoonThread 线程时间来感知中断从而结束。
TimeUnit.MILLISECONDS.sleep(10);
thread.interrupt();

// 打印结果:
// i = 1
// i = 2
// ...
// i = 699
// i = 700
// stop // 执行到了 run 方法的末尾,线程即将终止。
}

public static class MoonRunner implements Runnable{
private long i;
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
i++;
System.out.println("i = " + i);
}
System.out.println("stop");
}
}
}

2、采用 boolean 变量来控制是否需要停止线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class StopThread {

public static void main(String[] args) throws InterruptedException {
MoonRunner moonRunner = new MoonRunner();
Thread thread = new Thread(moonRunner,"MoonThread");
thread.start();
// 调用 sleep 方法使得 main 线程睡眠 10 ms,这是为了留给 MoonThread 线程时间来感知中断从而结束。
TimeUnit.MILLISECONDS.sleep(10);
moonRunner.cancel();

// 打印结果:
// i = 1
// i = 2
// ...
// i = 714
// i = 715
// stop // 执行到了 run 方法的末尾,线程即将终止。
}

public static class MoonRunner implements Runnable{
private long i;
// 因为涉及多个线程对这个变量的访问,所以在设置 volatile boolean on 的时候,
// 当有其他线程改变其值时,所有的线程都会感知到它的变化。
private volatile boolean on = true;
@Override
public void run() {
while (on){
i++;
System.out.println("i = " + i);
}
System.out.println("stop");
}

public void cancel(){
on = false;
}
}
}

同步

在多线程应用中,两个或两个以上的线程需要共享对同一个数据的存取。如果存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。

竞争条件最容易理解的例子为:比如车站售卖火车票,票数一定,但售卖窗口很多,每个窗口就相当于一个线程。这么多的线程共用所有的火车票资源,如果不使用同步是无法保证其原子性的。解决的办法如下:当一个线程要使用火车票这个资源时,就交给它一把锁,等它把事情做完后再把锁给另一个要用这个资源的线程。

重入锁与条件对象

synchronized 关键字自动提供了锁以及相关的条件。大多数需要显式锁的情况使用 synchronized 非常方便,而了解重入锁和条件对象时,能更好的地理解 synchronized 关键字。重入锁 ReentrantLock 是 Java SE 5.0 引入的,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。用 ReentrantLock 保护代码块的结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
Lock lock = new ReentrantLock();
// 当一个线程封锁了锁对象,其它任何线程都无法进入 Lock 语句。
lock.lock();
try{
// ...
}...
finally {
// 在 finally 中进行解锁操作。
// 意义在于,如果临界区发生了异常,锁是必须要释放的,否则其它线程将会永远被阻塞。
lock.unlock();
}

这一结构确保任何时刻只有一个线程进入临界区,临界区就是在同一时刻只能有一个任务访问的代码区。当进入临界区时,却发现在某个条件满足之后,它才能执行。这时可以使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程,条件对象又被称作条件变量。

同步方法

Lock 和 Condition 接口为程序设计人员提供了高度的锁定控制,然而大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到 Java 语言内部的机制。从 Java 1.0 版开始,Java 中的每一个对象都有一个内部锁。如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,线程必须获得内部的对象锁。

同步代码块

每一个 Java 对象都有一个锁,线程可以调用同步方法来获得锁。而使用同步代码块,是另一种可以获得锁的机制。通常不推荐使用。

volatile

有时仅仅为了读写一个或者两个实例域就使用同步的话,显得开销过大。而 volatile 关键字为实力域的同步访问提供了免锁的机制。如果声明一个域为 volatile ,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

首先了解下内存模型的相关概念,以及并发编程中的 3 个特性:原子性、可见性、有序性。

Java 内存模型

Java 中的堆内存用来存储对象实例,堆内存是被所有线程共享的运行时内存区域,因此,它存在内存可见性的问题。而局部变量、方法定义和参数则不会在线程之间共享,它们不会有内存可见性的问题,也不受内存模型的影响。Java 内存模型定义了线程和主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。需要注意的是本地内存是 Java 内存模型的一个抽象概念,其并不真实存在,它涵盖了缓存、写缓冲区、寄存器等区域。Java 内存模型控制线程之间的通信,它决定一个线程对主存共享变量的写入何时对另一个线程可见。

线程 A 与 线程 B 之间若要通信,必须经历以下两个步骤:

  • 线程 A 把线程 A 本地内存中更新过的共享变量刷新到主存中去。

  • 线程 B 到主存中去读取线程 A 之前已更新过的共享变量。由此可见,如果执行下面的语句。

    1
    2
    // 执行线程必须先在自己的工作线程中对变量 i 所在的缓存行进行赋值操作,然后再写入主存当中,而不是直接将数值 3 写入主存当中。
    int i = 3;

原子性

对基本数据类型变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行完毕,要么就不执行。

1
2
3
4
5
6
// 只有这条语句是原子性操作,其它两个语句都不是原子性操作。
x = 3;
// 包含了 2个操作:它先读取 x 的值,再将 x 的值写入工作内存。读取 x 的值以及将 x 的值写入工作内存这两个操作单拿出来都是原子性操作,但合起来就不是原子性操作了。
y = x;
// 包括了 3 个操作:读取 x 的值,对 x 的值进行加 1,向工作内存写入新值。
x++;

可知,一个语句含有多个操作时,就不是原子性操作,只有简单地读取和赋值(将数字赋值给某个变量)才是原子性操作。(java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。例如 AtomicInteger 类提供了方法 incrementAndGet 和 decrementAndGet,它们分别以原子方式将一个整数自增和自减。可以安全地使用 AtomicInteger 类作为共享计数器而无需同步。另外这个包还包含 AtomicBoolean、AtomicLong 和 AtomicReference 这些原子类,这仅供开发并发工具的系统程序员使用,应用程序员不应该使用这些类。)

可见性

指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。当一个共享变量被 volatile 修饰时,它会保证修改的值立即被更新到主存,所以对其它线程是可见的。当有其它线程需要读取该值时,其它线程会去主存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,并不会立即被写入主存,何时被写入主存也是不确定的。当其他线程去读取该值时,此时主存中可能还是原来的旧值,这样就无法保证可见性。

有序性

Java 内存模型中允许编译器和处理器对指令进行重排序,虽然重排序过程不会影响到单线程执行的正确性,但是会影响到多线程并发执行的正确性。这时可通过 volatile 来保证有序性,除了 volatile,也可以通过 synchronize 和 Lock 来保证有序性。synchronize 和 Lock 保证每个时刻只有一个线程执行同步代码,这相当于是让线程顺序执行同步代码,从而保证了有序性。

volatile 关键字

当一个共享变量被 volatile 修饰之后,其就具备了两个含义,一个是线程修改了变量的值时,变量的新值对其它线程是立即可见的。换句话说,就是不同线程对这个变量进行操作时具有可见性。另一个含义是禁止使用指令重排序。(重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译器重排序和运行期重排序,分别对应编译时和运行时环境。)

volatile 不保证原子性,保证有序性。

阻塞队列

阻塞队列(BlockingQueue)常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。


实现线程的方式

一般实现线程的方法有 3 种,其中前两种最为常用:

  • 继承 Thread ,并重写 run 方法。
  • 实现 Runnable 接口,并实现该接口的 run 方法。
  • 实现 Callable 接口,并重写 call 方法。

一般推荐用实现 Runnable 接口的方式,其原因是,一个类应该在其需要加强或修改时才会被继承。因此如果没有必要重写 Thread 类的其他方法,那么在这种情况下最好用实现 Runnable 接口的方式。而且 Java 是单继承但可以调用多个接口,所以看起来实现接口更加好一些。

继承 Thread

Thread 本质上也是实现了 Runnable 接口的一个实例。需要注意当调用 start 方法后并不是立即执行线程的代码,而是使线程变为可运行状态,什么时候运行代码是由操作系统决定的。其主要步骤如下:

  • 定义 Thread 类的子类,并重写该类的 run 方法,该 run 方法的方法体就代表了线程要完成的任务。因此,run 方法被称为执行体。
  • 创建 Thread 子类的实例,即创建了线程对象。
  • 调用线程对象的 start 方法来启动该线程。

###实现 Runnable 接口

其主要步骤如下:

  • 自定义类并实现 Runnable 接口,实现 run 方法。
  • 创建 Thread 子类的实例,用实现 Runnable 接口的对象作为参数实例化该 Thread 对象。
  • 调用 Thread 的 start 方法来启动该线程。

实现 Callable 接口

Callable 接口实际是属于 Executor 框架中的功能类,Callable 接口与 Runnable 接口的功能类似,但提供了比 Runnable 更强大的功能,主要表现为以下 3 点:

  • Callable 可以在任务接受后提供一个返回值,而 Runnable 无法提供这个功能。
  • Callable 中的 call 方法可以抛出异常,而 Runnable 的 run 方法不能抛出异常。
  • 运行 Callable 可以拿到一个 Future 对象,Future 对象表示异步计算的结果,它提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下就可以使用 Future 来监视目标线程调用 call 方法的情况。但调用 Future 的 get 方法以获取结果时,当前线程就会阻塞,直到 call 方法返回结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestCallable {
/**
* 创建线程类
*/
public static class MyTestCallable implements Callable{
@Override
public String call() throws Exception {
return "Hello World";
}
}

public static void main(String[] args) {
MyTestCallable myTestCallable = new MyTestCallable();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(myTestCallable);

try{
// 等待线程结束,并返回结果。
System.out.println(future.get());
}catch (Exception e){
e.printStackTrace();
}
}
}

示例(Java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initData();
}

private void initData() {
// 继承 Thread
ExtendsThread extendsThread = new ExtendsThread();
// 当调用 Thread 时,会有两种方式:
// run()方法只是调用了 Thread 实例的 run() 方法而已,它仍然运行在主线程上。
// start()方法会开辟一个新的线程,在新的线程上调用 run() 方法,此时它运行在新的线程上。
extendsThread.start(); // 运行结果:1
extendsThread.run(); // 运行结果:658

// 实现 Runnable 接口
ImplementsThread implementsThread = new ImplementsThread();
new Thread(implementsThread).start(); // 运行结果:659

// 实现 Callable 接口
// Thread 类只支持 Runnable 接口,由此引入 FutureTask 的概念
// 运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。
// 它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
// 通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果;
ExecutorService executor = Executors.newCachedThreadPool();
CallableThread task = new CallableThread();
Future<Integer> result = executor.submit(task);
executor.shutdown(); // 运行结果:660

// FutureTask 实现了 Runnable 和 Future,所以兼顾两者优点,
// 既可以在 Thread 中使用,又可以在 ExecutorService 中使用。
// 它可以让程序员准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果。
// FutureTask 是一种可以取消的异步的计算任务,它的计算是通过 Callable 实现的,
// 它等价于可以携带结果的 Runnable,并且有三个状态:等待、运行和完成。
// 完成包括所有计算以任意的方式结束,包括正常结束、取消和异常。
// 第一种方式
ExecutorService executor1 = Executors.newCachedThreadPool();
CallableThread task1 = new CallableThread();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task1);
executor1.submit(futureTask);
executor1.shutdown(); // 运行结果:661

// 第二种方式,注意这种方式和第一种方式效果是类似的,
// 只不过一个使用的是ExecutorService,一个使用的是Thread
CallableThread task2 = new CallableThread();
FutureTask<Integer> futureTask2 = new FutureTask<Integer>(task2);
Thread thread = new Thread(futureTask2);
thread.start(); // 运行结果:662

try {
Log.e("TAG","CallableThread结果:"+result.get()); // 运行结果:1
Log.e("TAG","CallableThread结果:"+futureTask.get()); // 运行结果:1
Log.e("TAG","CallableThread结果:"+futureTask2.get()); // 运行结果:1
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}

/**
* 继承 Thread
*/
class ExtendsThread extends Thread{
@Override
public void run() {
super.run();
Log.e("TAG","ExtendsThread:"+Thread.currentThread().getId());
}
}

/**
* 实现 Runnable 接口
* JDK 1.0 增加
* Thread 调用了 Runnable 接口中的方法用来在线程中执行任务。
*/
class ImplementsThread implements Runnable{
@Override
public void run() {
Log.e("TAG","ImplementsThread:"+Thread.currentThread().getId());
}
}

/**
* 实现 Callable 接口
* Callable 一般是和 ExecutorService 配合来使用的
* JDK1.5 增加
*
* 相比较 Runnable 接口:
* Callable 接口下的方法是 call(),Runnable 接口的方法是 run();
* Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的;
* call() 方法可以抛出异常,run()方法不可以的;
*/
class CallableThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Log.e("TAG","CallableThread:"+Thread.currentThread().getId());
return 1;
}
}

}

示例(Kotlin)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

class ThreadActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_thread)

initView()
initData()
}

/**
* 定义一个整型变量,用来表示更新 TextView 这个动作
*/
val updateText = 1

private fun initView() {
btnChangeText.setOnClickListener{
val msg = Message()
msg.what = updateText
// 将 Message 对象发送出去
handler.sendMessage(msg)
}
}

/**
* 新增一个 Handler 对象,并重写父类的 handleMessage()
*/
val handler = object : Handler(){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
// 进行 UI 操作
when(msg.what){
updateText -> tvText.text = "Nice to meet you"
}
}
}

private fun initData() {
// 启动线程
MyThread().start()

val myThread = MyThread2()
// Thread 构造函数接收一个 Runnable 参数
Thread(myThread).start()

// 如果不想专门定义一个类去实现 Runnable 接口,
// 可以使用 Lambda 的方式,这种写法更为常见。
Thread{
// 编写具体的逻辑
Log.e("TAG","匿名 Thread is start。")
}.start()

// Kotlin 提供了一种更加简单的开启线程的方式,
// 这里的 Thread 是一个 Kotlin 内置的顶层函数,
// 只需要在 Lambda 表达式中编写具体的逻辑即可。
thread {
// 编写具体的逻辑
Log.e("TAG","Kotlin Thread is start。")
}
}

/**
* 创建类继承自 Thread,并重新父类的 run()。
*/
class MyThread:Thread(){
override fun run() {
super.run()
// 编写的逻辑
Log.e("TAG","类继承 Thread is start。")
}
}

/**
* 更加推荐使用实现 Runnable 接口的方式来定义一个线程,
* 因为继承的方式耦合性有点高。
*/
class MyThread2:Runnable{
override fun run() {
// 编写的逻辑
Log.e("TAG","实现接口 Thread is start。")
}
}

companion object{
fun actionStart(context: Context){
val intent = Intent(context, ThreadActivity::class.java)
context.startActivity(intent)
}
}
}

主线程和子线程

从用途上来说,线程分为主线程(主要处理和界面相关的事情)和子线程(往往用于执行耗时操作)。

主线程是指进程所拥有的线程,在 Java 中默认一个进程只有一个线程,即主线程。子线程也叫工作线程,除主线程以外的线程都是子线程。

Android 沿用了 Java 的线程模型,其中主线程也叫 UI 线程,作用是运行四大组件以及处理它们和用户的交互,因为所有的UI控件操作都在UI线程中执行,用户随时会和界面发生交互,因此主线程要有较高的响应速度。如果在主线程执行耗时任务,会阻塞UI线程,甚至导致ANR错误,所以对耗时任务需要创建工作线程来执行。而子线程的作用就是执行耗时任务。

Android中哪些场景是执行在主线程的?
1、Activity生命周期回调都是执行在主线程的.
2、Service默认是执行在主线程的.
3、BroadcastReceiver的onReceive回调是执行在主线程的.
4、没有使用子线程的looper的Handler的handleMessage, post(Runnable)是执行在主线程的.
5、AsyncTask的回调中除了doInBackground, 其他都是执行在主线程的.
6、View的post(Runnable)是执行在主线程的.等等

Android开发中何时使用多进程?使用多进程的好处是什么?
要想知道如何使用多进程,先要知道Android里的多进程概念。一般情况下,一个应用程序就是一个进程,这个进程名称就是应用程序包名。我们知道进程是系统分配资源和调度的基本单位,所以每个进程都有自己独立的资源和内存空间,别的进程是不能任意访问其他进程的内存和资源的。

那如何让自己的应用拥有多个进程?
很简单,我们的四大组件在AndroidManifest文件中注册的时候,有个属性是android:process,
1.这里可以指定组件的所处的进程。默认就是应用的主进程。指定为别的进程之后,系统在启动这个组件的时候,就先创建(如果还没创建的话)这个进程,然后再创建该组件。你可以重载Application类的onCreate方法,打印出它的进程名称,就可以清楚的看见了。再设置android:process属性时候,有个地方需要注意:如果是android:process=”:deamon”,以:开头的名字,则表示这是一个应用程序的私有进程,否则它是一个全局进程。私有进程的进程名称是会在冒号前自动加上包名,而全局进程则不会。一般我们都是有私有进程,很少使用全局进程。他们的具体区别不知道有没有谁能补充一下。
2.使用多进程显而易见的好处就是分担主进程的内存压力。我们的应用越做越大,内存越来越多,将一些独立的组件放到不同的进程,它就不占用主进程的内存空间了。当然还有其他好处,有心人会发现Android后台进程里有很多应用是多个进程的,因为它们要常驻后台,特别是即时通讯或者社交应用,不过现在多进程已经被用烂了。典型用法是在启动一个不可见的轻量级私有进程,在后台收发消息,或者做一些耗时的事情,或者开机启动这个进程,然后做监听等。还有就是防止主进程被杀守护进程,守护进程和主进程之间相互监视,有一方被杀就重新启动它。应该还有还有其他好处,这里就不多说了。
3.坏处的话,多占用了系统的空间,大家都这么用的话系统内存很容易占满而导致卡顿。消耗用户的电量。应用程序架构会变复杂,应为要处理多进程之间的通信。这里又是另外一个问题了。


Android 中线程形态

除了 Thread 本身以外,在 Android 中充当线程的角色还有 AsyncTask、HandlerThread、IntentService。它们本质上都是由 Handler + Thread 来构成的(AsyncTask 的底层用到了线程池),不过不同的设计让它们可以在不同的场合发挥更好的作用。

  • AsyncTask:封装了线程池和 Handler,主要为了方便开发者在子线程中更新 UI。
  • HandlerThread:是一种具有消息循环队列的线程,可以方便在子线程中处理不同的事务,在它的内部可使用 Handler。
  • IntentService:是一个服务,系统对其封装使其可以更方便地执行后台任务,IntentService 内部采用 HandlerThread 来执行任务,任务完毕后 IntentService 会自动退出。它的优势在于不易被系统杀死。

AsyncTask(Android 11 被废弃)

为了更加方便我们在子线程中对 UI 进行操作,Android 还提供了另外一些好用的工具,比如 AsyncTask。

是一种轻量级的异步任务类,它可在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并在主线程中更新 UI。其封装了 Thread 和 Handler。并且不适合做特别耗时的后台任务。

AsyncTask 是一个抽象的泛型类,提供了 3 个泛型参数,如不需要可用 Void 代替:

  • Params:在执行 AsyncTask 时需要传入的参数,可用于在后台任务中使用。
  • Progress:在后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
  • Result:当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

AsyncTask 的四个核心方法,执行顺序如下:

  • onPreExecute():主线程中执行,会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框。
  • doInBackground(Params…params):子线程中运行,在线程池中执行,此方法用于异步耗时任务,params 参数表示异步任务的输入参数。在此方法中可通过 publishProgress() 来更新任务的进度,publishProgress() 会调用 onProgressUpdate()。另外此方法需要返回计算结果给 onPostExecute()。
  • onProgressUpdate(Progress…values):在主线程中执行,当在后台任务中调用 publishProgress() 后,执行进度改变时此方法会快会被调用,该方法中携带的参数就是在后台任务中传递过来的。
  • onPostExecute(Result…result):在主线程中执行,在异步任务执行之后,此方法会被调用,其中 result 参数是后台任务的返回值, 即 doInBackground 的返回值。当 onCancelled()(主线程执行,异步任务取消时)被调用时,此方法不会被调用。

AsyncTask 在具体使用过程中的限制:

  • 第一次访问 AsyncTask 必须在主线程,不过这个过程在 Android 4.1 及以上版本中已经被系统自动完成。
  • AsyncTask 的对象必须在主线程中创建。
  • execute() 必须在 UI 线程调用。
  • 不要在程序中直接调用四个方法。
  • 一个 AsyncTask 对象只能执行一次,即只能调用一次 execute()。
  • 在 Android 1.6 之前和 Android 3.0 开始(可通过 executeOnExecutor() 并行执行任务),AsyncTask 是串行执行任务的。Android 1.6 时开始采用线程池里处理并行任务。

示例(Java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* AsyncTask 是一个抽象泛型类,
* 声明:public abstract class AsyncTask<Params, Progress, Result>;
* 参数1,Params,异步任务的入参;
* 参数2,Progress,执行任务的进度;
* 参数3,Result,后台任务执行的结果;
*/
public class MyAsyncTask extends AsyncTask {

private String mName;

public MyAsyncTask(String mName){
this.mName = mName;
}

/**
* 在主线程中执行,任务开启前的准备工作;
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
}

/**
* 开启子线程执行后台任务;
*/
@TargetApi(Build.VERSION_CODES.N)
@Override
protected Object doInBackground(Object[] objects) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Log.e(mName, dateFormat.format(new Date(System.currentTimeMillis())));
return null;
}

/**
* 在主线程中执行,更新UI进度;
*/
@Override
protected void onProgressUpdate(Object[] values) {
super.onProgressUpdate(values);
}

/**
* 在主线程中执行,
* 异步任务执行完成后执行,
* 它的参数是 doInbackground() 的返回值。
*/
@Override
protected void onPostExecute(Object o) {
super.onPostExecute(o);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initAsyncTask();
}

private void initAsyncTask() {
// 串行
(new MyAsyncTask("MyAsyncTask01")).execute();
(new MyAsyncTask("MyAsyncTask02")).execute();
(new MyAsyncTask("MyAsyncTask03")).execute();

// 并行
(new MyAsyncTask("MyAsyncTask04")).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
(new MyAsyncTask("MyAsyncTask05")).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
(new MyAsyncTask("MyAsyncTask06")).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");

// E/MyAsyncTask01: 2019-06-27 15:13:46
// E/MyAsyncTask06: 2019-06-27 15:13:46
// E/MyAsyncTask04: 2019-06-27 15:13:46
// E/MyAsyncTask05: 2019-06-27 15:13:46
// E/MyAsyncTask02: 2019-06-27 15:13:47
// E/MyAsyncTask03: 2019-06-27 15:13:48
}

}

示例(Kotlin):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* @param Unit 表示在执行 AsyncTask 时不需要传入参数给后台任务
* @param Int 表示使用整型数据来作为进度显示单位
* @param Boolean 表示使用布尔型数据来反馈执行结果
*
* 使用 AsyncTask 的诀窍就是:
* 在 doInBackground() 中执行具体的耗时任务,
* 在 onProgressUpdate() 中进行 UI 操作。
* 在 onPostExecute() 中执行一些任务的收尾工作。
*
* 启动这个任务:DownloadTask.execute()
* 并且可以传入任意数量的参数,这些参数将会传递到 DownloadTask 的 doInBackground() 中。
*/
class DownloadTask: AsyncTask<Unit, Int, Boolean>(){

override fun onPreExecute() {
progressDialog.show() // 显示进度对话框
}

/**
* 执行具体的下载任务,在子线程中运行。
*
* 如果 AsyncTask 的第三个泛型参数指定的是 Unit,就可以不返回任务执行结果。
*
* 当下载完成后,会返回一个布尔型变量,onPostExecute() 很快被调用。
*/
override fun doInBackground(vararg params: Unit?) = try {
while (true){
val downloadPercent = doDownload() // 这是一个虚构方法,用于计算当前的下载进度并返回。
// 得到进度后,调用方法并传入当前的下载进度,这样 onProgressUpdate() 很快会被调用。
// 切换线程
publishProgress(downloadPercent)
if (downloadPercent >= 100){
break
}
}
true
} catch (e:Exception){
false
}

/**
* 进行 UI 操作
*/
override fun onProgressUpdate(vararg values: Int?) {
// 在这里更新下载进度
progressDialog.setMessage("Downloaded ${values[0]}")
}

override fun onPostExecute(result: Boolean?) {
progressDialog.dismiss() // 关闭进度对话框
// 在这里提示下载结果
if (result){
Toast.makeText(content,"Download succeeded",Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(content,"Download failed",Toast.LENGTH_SHORT).show()
}
}
}

AsyncTask 的工作原理

execute() –> executeOnExecutor():

  • executeOnExecutor()
    sDefaultExecutor 是一个串行的线程池,一个进程中所有的 AsyncTask 全部在其中排队执行。
    会先执行 onPreExecute(),然后线程池开始执行。
  • 线程池 SerialExecutor
    首先会把 Params 参数封装为 FutureTask 对象,它是一个并发类,这里充当 Runnable 的作用。
    接着这个 FutureTask 会交给 execute() 处理,会把它插入到任务队列 mTasks 中,这时如果没有正在活动的 AsyncTask 任务,会调用 scheduleNext() 执行下一个 AsyncTask 任务。
    同时当一个 AsyncTask 任务执行完毕后,会继续执行其他任务,直到所有任务都被执行为止。

AsyncTask 中有两个线程池和一个 Handler:

  • SerialExecutor:用于任务的排队
  • THREAD_POOL_EXECUTOR:用于真正地执行任务
  • InternalHandler:用于将执行环境从线程池切换到主线程

在 AsyncTask 的构造方法中,FutureTask 的 run() 会调用 mWorker 的 call(),因此 call() 最终在线程池中执行:

  • call()
    将 mTaskInvoked 设为 true,表示当前任务已经被调用过了,然后执行 AsyncTask 的 doInBackground 方法,接着将其返回值传递给 postResult()。
  • postResult()
    会通过 sHandler 发送一个 MESSAGE_POST_RESULT 的消息。
  • sHandler 的定义
    一个静态的 Handler 对象,为了能够将执行环境切换到主线程,sHandler 必须在主线程中创建。
    由于静态成员会在加载类的时候进行初始化,因此这就变相要求 AsyncTask 的类也必须在主线程中加载,否则同一个进程中的 AsyncTask 都无法正常工作。
    收到 MESSAGE_POST_RESULT 消息后,会调用 AsyncTask 的 finish()。
  • finish()
    如果 AsyncTask 被取消执行了,就调用 onCancelled(),否则调用 onPostExecute(),可看到 doInBackground 的返回结果会传递给 onPostExecute()。
    到这里,AsyncTask 的整个工作过程就完毕了。

HandlerThread

继承了 Thread,实际上是一个允许使用 Handler的特殊线程。外界需要通过 Handler 的方式来通知它执行一个具体的任务。

普通线程在 run() 方法中执行耗时操作,而 HandlerThread 在 run() 方法创建了一个消息队列不停地轮询消息,可通过 Handler 发送消息来告诉线程该执行什么操作。

它在 run() 中通过 Looper.prepare() 来创建消息队列,并通过 Looper.loop() 来开启消息循环:

  • run()
    是一个无限循环,因此当不再使用 HandlerThread 时,可通过调用 quit() 或 quitSafely() 来终止线程的执行。

它常见的使用场景是在 IntentService 中。当不再需要 HandlerThread 时,通过调用 quit/Safely 方法来结束线程的轮询并结束该线程。

IntentService

一种特殊的 Service,它是一个继承 Service 的抽象类,所以必须实现它的子类再去使用。

IntentService 可以理解为它是一个实现了 HandlerThread 的 Service。

因为 Service 的优先级比较高,可以利用这个特性来保证后台服务的优先正常执行,甚至还可以为Service开辟一个新的进程。并且任务执行后会自动停止。

当第一次启动时,onCreate() 会被调用:

  • onCreate()
    会创建一个 HandlerThread,然后使用它的 Looper 来构造一个 Handler 对象 mServiceHandler,这样通过 mServiceHandler 发送的消息最终都会在 HandlerThread 中执行。

每次启动 IntentService,它的 onStartCommand() 就会调用一次,其中处理每个后台任务的 Intent:

  • onStartCommand()
    调用了 onStart()。
  • onStart()
    通过 mServiceHandler 发送了一个消息,会在 HandlerThread 中被处理。

mServiceHandler 收到消息后,会将 Intent 对象传递给 onHandleIntent()(一个抽象方法,需要在子类实现,它的作用是从 Intent 参数中区分具体的任务并执行这些任务)去处理,当 onHandleIntent() 执行结束后,IntentService 会通过 stopSelf(int startId)(区别于 stopSelf() 会立刻停止服务,它会等待所有消息处理完毕后才终止服务) 来尝试停止服务。


Android 中的线程池

池:同类对象的批量管理。

线程池:用少量线程,让线程保持忙碌。(线程处理完任务,回到线程池中等待,不结束。)

线程可以预先创建,让它进入阻塞状态,等待任务在唤醒。
BlockingQueue:

  • take()->取数据->如果这Queue中没数据->则会阻塞
  • put()->放数据->如果这Queue中满了->则会阻塞
  • 线程的run()中设置一个循环,每次从BlockingQueue中获取任务,空的则会阻塞,有消息通知线程池的某一线程处理,并且依然从Queue中获取任务。

一个线程池中会缓存一定数量的线程,通过线程池就可以避免因为频繁创建和销毁线程所带来的系统开销。Android 中的线程池来源于 Java,主要通过 Executor 来派生特定类型的线程池。

它的优点

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

ThreadPoolExecutor

它是线程池的真正实现,它的构造方法提供了一系列参数来配置线程池。
ExecutorService 是最初的线程池接口,ThreadPoolExecutor 类是对线程池的具体实现,它通过构造方法来配置线程池的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* corePoolSize
* 线程池中核心线程的数量,
* 默认情况下,即使核心线程没有任务在执行它也存在的,
* 我们固定一定数量的核心线程且它一直存活这样就避免了一般情况下CPU创建和销毁线程带来的开销。
* 我们如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程就会有超时策略,这个时间由keepAliveTime来设定,
* 即keepAliveTime时间内如果核心线程没有回应则该线程就会被终止。allowCoreThreadTimeOut默认为false,核心线程没有超时时间。
*
* maximumPoolSize
* 线程池中的最大线程数,当任务数量超过最大线程数时其它任务可能就会被阻塞。
* 最大线程数=核心线程+非核心线程。非核心线程只有当核心线程不够用且线程池有空余时才会被创建,执行完任务后非核心线程会被销毁。
*
* keepAliveTime
* 非核心线程的超时时长,当执行时间超过这个时间时,非核心线程就会被回收。
* 当allowCoreThreadTimeOut设置为true时,此属性也作用在核心线程上。
*
* unit
* 枚举时间单位,TimeUnit。
*
* workQueue
* 线程池中的任务队列,我们提交给线程池的runnable会被存储在这个对象上。
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}

它执行任务时的大致规则

  1. 如果线程池中的线程数量未达到核心线程的数量,那么会直接启动一个核心线程来执行任务。
  2. 如果线程池中的线程数量已经达到或者超过核心线程的数量,那么任务会被插入到任务队列中排队等待执行。
  3. 如果在步骤 2 中无法将任务插入到任务队列中,这往往是由于任务队列已满,这个时候如果线程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行任务。
  4. 如果步骤 3 中线程数量已经达到线程池规定的最大值,那么就拒绝执行此任务,ThreadPoolExecutor 会调用 RejectedExecutionHandler 的 rejectedExecution() 来通知调用者。

线程池的分类

一些不同特性的线程池,它们都直接或者间接通过 ThreadPoolExecutor 来实现自己的功能
Android 中常见的四类线程池

  • FixedThreadPool
    通过 Executors 的 newFixedThreadPool() 来创建。它是一种线程数量固定的线程池,当线程处于空闲状态时,它们并不会被回收,除非线程池被关闭了。当所有的线程都处于活动状态时,新任务都会处于等待状态,直到有线程空闲出来。它能更快的响应外界请求。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    通过 Executors 的 newFixedThreadPool() 方法创建,
    它是个线程数量固定的线程池,该线程池的线程全部为核心线程,
    它们没有超时机制且排队任务队列无限制,因为全都是核心线程,所以响应较快,且不用担心线程会被回收。
    参数 nThreads,就是固定的核心线程数量。
    */
    public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());
    }
  • CachedThreadPool
    是一种线程数量不定的线程池,它只有非核心线程,并且其最大线程数为 Integer.MAX_VALUE(很大的数)。当线程池中的线程都处于活动状态时,线程池会创建新的线程来处理新任务,否则就会利用空闲的线程来处理新任务。空闲线程都有超时机制(60 秒),超过就会被回收。它的任务队列相当于一个空集合,导致任何任务都会被立即执行。适合执行大量的耗时较少的任务。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    通过Executors的newCachedThreadPool()方法来创建,
    它是一个数量无限多的线程池,它所有的线程都是非核心线程,
    当有新任务来时如果没有空闲的线程则直接创建新的线程不会去排队而直接执行,并且超时时间都是60s,所以此线程池适合执行大量耗时小的任务。
    由于设置了超时时间为60s,所以当线程空闲一定时间时就会被系统回收,所以理论上该线程池不会有占用系统资源的无用线程。
    */
    public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>());
    }
  • ScheduledThreadPool
    它的核心线程数量是固定的,而非核心线程数是没有限制的,并且当非核心线程闲置时会被立即回收。主要用于执行定时任务和具有固定周期的重复任务。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    通过Executors的newScheduledThreadPool()方法来创建,
    ScheduledThreadPool线程池像是上两种的合体,它有数量固定的核心线程,且有数量无限多的非核心线程,
    但是它的非核心线程超时时间是0s,所以非核心线程一旦空闲立马就会被回收。
    这类线程池适合用于执行定时任务和固定周期的重复任务。
    参数corePoolSize是核心线程数量。
    */
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
    }

    public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
    DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
    new DelayedWorkQueue());
    }
  • SingleThreadExecutor
    这类线程池内部只有一个核心线程,它确保所有的任务都在同一个线程中按顺序执行。它的意义在于统一所有的外界任务到一个线程中,这使得在这些任务之间不需要处理线程同步的问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    通过Executors的newSingleThreadExecutor()方法来创建,
    它内部只有一个核心线程,它确保所有任务进来都要排队按顺序执行。
    它的意义在于,统一所有的外界任务到同一线程中,让调用者可以忽略线程同步问题。
    */
    public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
    (new ThreadPoolExecutor(1, 1,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>()));
    }

线程池一般用法

  • shutDown():关闭线程池,需要执行完已提交的任务。
  • shutDownNow():关闭线程池,并尝试结束已提交的任务。
  • allowCoreThreadTimeOut(boolen):允许核心线程闲置超时回收。
  • execute():提交任务无返回值。接收一个Runnable对象作为参数,异步执行。
    1
    2
    3
    4
    5
    6
    7
    Runnable myRunnable = new Runnable() {
    @Override
    public void run() {
    Log.e("myRunnable", "run");
    }
    };
    mExecutor.execute(myRunnable);
  • submit():提交任务有返回值。

备注

参考资料:

线程池的好处

第一行代码(第3版)

Android 开发艺术探索

Android 进阶之光

单词音标:
  • fixed 英 [fɪkst] 美 [fɪkst]
  • cached 英 [kæʃt] 美 [kæʃt]
  • scheduled 英 ['ʃedjuːld] 美 [ˈskedʒuːld]
  • single 英 [ˈsɪŋɡl] 美 [ˈsɪŋɡl]
  • async 英 [əˈsɪŋk] 美 [æˈsɪŋk]
  • executor 英 [ɪɡˈzekjətə(r)] 美 [ɪɡˈzekjətər]