谁不会休息,谁就不会工作。 — 列宁
写在前面
Android沿用了Java的线程模型,一个Android应用创建的时候就会开启一个线程,我们叫它主线程或者UI线程,如果想要访问网络或数据库等耗时操作时,就会开启一个子线程去处理。从Android3.0开始,系统规定网络访问必须在子线程中进行,否则会抛出异常。就是为了避免因耗时操作阻塞主线程从而发生ANR,也证明了多线程在Android应用开发中占据着十分重要的位置。
进程和线程
在了解线程之前,我们先来了解一下进程,什么是进程?进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统资源分配和调度的基本单元,进程可以被看做是程序的实体,进程也是线程的容器。这么说过于抽象,下面打开我们电脑的任务管理器:
从图片中可以看到“应用”下面包含三个前台进程,“后台进程”有五十七个,每一个进程就对应一个应用。
上面图片中"WeChat(32)位"对应微信这个应用的进程,这个进程里又运行了许多子任务,有的处理缓存,有的进行下载,有的接收消息,这些子任务就是线程,是操作系统调度的最小单元,也叫做轻量级的进程。一个进程可以创建多个线程,可以创建的线程数量取决于操作系统,每个线程都拥有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存数据。
为何使用多线程
从操作系统级别上来看主要有以下四个方面:
- 使用多线程能够减少程序响应的时间。如果某个操作耗时,或是陷入长时间的等待,就不能响应鼠标或键盘等操作,使用多线程能够将耗时操作放到一个单独的线程中执行,从而使程序具备了更好的交互性。
- 与进程相比,线程的创建和切换开销更小,在访问共享数据方面效率非常高。
- 多CPU或多核计算机本身就具备执行多线程的能力。如果执行单个线程,就无法重复利用计算机的资源,从而导致巨大的资源被浪费,在多CPU的计算机中使用多线程能够有效提升CPU的利用率。
- 使用多线程能够简化程序结构,使程序便于理解和维护。
线程的状态
Java线程在运行的生命周期中会有六种不同的状态:
- New:新创建状态。线程创建,但还没有调用start函数,线程运行之前还有一些基础工作要做。
- Runnable:可运行状态。一旦调用start函数,线程就处于可运行状态,可运行状态的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
- Blocked:阻塞状态。表示线程被锁阻塞,暂不活动。
- Waiting:等待状态。表示线程暂不活动,并且不运行任何的代码,直到线程调度器重新激活它。
- Timed Waiting:超时等待状态。与等待状态不同,超时等待状态的线程会在指定的时间自行返回。
- Terminated:终止状态。表示线程执行完毕,终止线程有两种情况,第一种是线程的run函数执行完毕正常退出,第二种情况是因为一个没有捕获的异常而终止了run函数,导致线程进入终止状态。
从上图可以看到新创建状态(NEW)调用start()函数进入到可运行状态(RUNNABLE)。调用wait()等函数使可运行状态(RUNNABLE)的线程进入到等待状态(WAITING),调用notify()等函数使等待状态(WAITING)的线程回到可运行状态(RANNABLE)。超时等待状态(TIMED WAITING)就是在等待状态(WAITING)的基础上加入了时间的限制,达到指定的时间线程会从超时等待状态(TIMED WAITING)自行回到可运行状态(RUNNABLE)。调用同步方法时,线程获取不到锁就会进入阻塞状态(BLOCKED),直到线程再次获取到锁才会从阻塞状态(BLOCKED)回到可运行状态(RUNNABLE)。直到run()函数执行完毕或发生异常意外终止,线程才会变为终止状态(TERMINATED)。
创建线程
多线程的实现方法一般有三种,其中前两种为最常用的方法。
1.继承Thread类,重写run()方法
Thread类本质上是实现了Runnable接口的一个实现类,需要注意的是调用了start()方法不会立即执行多线程的代码,而是让该线程变为可运行状态,什么时候运行多线程的代码是由操作系统决定的。
实现步骤:
- 定义一个Thread的子类,并重写run()方法,该run()方法的方法体就代表了该线程要完成的任务,因此,run()方法被称为执行体。
- 创建Thread的子类对象,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
public static void main(String[] args) {
Thread thread = new ChildThread();
thread.start();
}
private static class ChildThread extends Thread {
@Override
public void run() {
super.run();
System.out.print("执行体");
}
}
2.实现Runnable接口,并实现该接口的run方法
实现步骤:
- 定义一个类并实现Runnable接口,实现run()方法。
- 定义Thread的子类对象,并使用实现Runnable接口的对象作为参数实例化该Thread的子类对象。
- 调用Thread的子类对象的start()方法来开启线程。
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
Thread thread = new Thread(runnable);
thread.start();
}
private static class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.print("执行体");
}
}
3.实现Callable接口,实现call()方法
Callable接口实际上是Executor框架中的功能类,Callable接口和Runnable接口功能类似,但提供了比Runnable接口更强大的功能,主要体现在以下三个方面:
- Callable接口可以在任务结束后提供一个返回值,Runnable接口无法提供这个功能。
- Callable接口中的call()方法能抛出异常,Runnable接口中的run()方法不能抛出异常。
- 运行Callable接口可以得到一个Futrue对象,Futrue对象表示异步计算的结果,它提供了检查异步计算是否完成的方法。由于线程属于异步计算模型,因此无法从其他线程得到函数的返回值,这种情况就需要Future对象监视目标线程执行call()方法的情况。但调用Futrue的get()方法以获取结果时,当前线程就会阻塞,直到call()方法返回结果。
实现步骤:
- 定义一个类实现Callable接口,并实现call()方法将计算结果作为返回值。
- 创建线程池ExecutorService,使用实现Callable接口的对象作为参数传给ExecutorService的submit(callable)来开启线程,并接收Futrue对象。
- 调用Futrue对象的get()方法获取结果。
public static void main(String[] args) {
CallableImpl callable = new CallableImpl();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(callable);
try {
String result = (String) future.get();
System.out.print(result);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static class CallableImpl implements Callable {
@Override
public String call() throws Exception {
return "计算结果";
}
}
中断线程
当线程的run方法执行完毕,或者在方法中出现了没有捕获的异常时,线程将终止。
在Java的早期版本中提供了一个stop方法,其他线程调用stop方法就会终止线程,由于该方法会带来一些问题,现在已经被弃用了。
interrupt方法可以用来请求中断线程,一个线程调用interrupt方法就会将线程的中断标识位置为true,线程会不时的检测线程的中断标识位以判断线程是否可以被中断。要想知道线程是否可以被中断,可以调用Thread.currentThread().isInterrupted()方法。
还可以调用interrupted方法对线程的中断标识位进行复位,但是如果线程被阻塞,将无法判断中断状态。如果一个线程处于阻塞状态,线程在检测线程的中断标识位时发现线程的中断标识位为true,就会在方法调用出抛出InterruptedException异常,并且在抛出异常前将线程的中断标识位重置,即设置为false。
需要注意的是被中断的线程并不一定会终止,中断线程只是为了引起线程的注意,被中断的线程决定是否响应中断。如果是非常重要的线程则不理会中断,但大部分情况是线程将中断作为一个终止的请求。另外,不要在底层代码中捕获InterruptedException异常后不做处理。如下:
private static class ChildThread extends Thread {
@Override
public void run() {
super.run();
try {
sleep(500);
} catch (InterruptedException e) {
// do something
}
}
}
推荐两种捕获InterruptedException异常后正确的处理方式:
- 第一种方式
public static void main(String[] args) {
Thread thread = new ChildThread();
// 开启子线程
thread.start();
// 让当前线程阻塞20毫秒
TimeUnit.MILLISECONDS.sleep(20);
// 设置线程的中断标识位为true
thread.interrupt();
}
private static class ChildThread extends Thread {
@Override
public void run() {
super.run();
doing();
}
private void doing() {
// 让子线程阻塞500毫秒,500毫秒之内如果调用了子线程的interrupt方法,
// 就会抛出InterruptedException异常,在方法内部直接捕获。
try {
sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
- 第二种方式
public static void main(String[] args) {
Thread thread = new ChildThread();
// 开启子线程
thread.start();
// 让当前线程阻塞20毫秒
TimeUnit.MILLISECONDS.sleep(20);
// 设置线程的中断标识位为true
thread.interrupt();
}
private static class ChildThread extends Thread {
@Override
public void run() {
super.run();
// 调用者捕获InterruptedException异常
try {
doing();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void doing() throws InterruptedException{
// 让子线程阻塞500毫秒,500毫秒之内如果调用了子线程的interrupt方法,
// 就会抛出InterruptedException异常,将异常向外抛出。
sleep(500);
}
}
由于抛出InterruptedException异常之前会将线程的中断标识位复位,即设置为false,所以在catch语句中再调用一次Thread.currentThread().interrupt()方法将线程的中断标识位设置为true,这样外部再次调用Thread.currentThread().isInterrupted()方法时就知道线程应该被中断了。
安全的中断线程
在前面了解到了如何中断线程,现在就来看看如何安全的中断线程,这里介绍两种安全的中断线程的方式。
- 第一种方式
public static void main(String[] args) {
Thread thread = new ChildThread();
// 开启子线程
thread.start();
// 让当前线程阻塞500毫秒
TimeUnit.MILLISECONDS.sleep(500);
// 设置线程的中断标识位为true
thread.interrupt();
}
private static class ChildThread extends Thread {
@Override
public void run() {
super.run();
// 判断线程的中断标识位是否为中断状态,
// 如果没有被中断则会一直调用doing,否则会被中断
while (!Thread.currentThread().isInterrupted()) {
doing();
}
}
private void doing() {
System.out.print("执行任务");
}
}
- 第二种方式
public static void main(String[] args) {
ChildThread thread = new ChildThread();
// 开启子线程
thread.start();
// 让当前线程阻塞500毫秒
TimeUnit.MILLISECONDS.sleep(500);
// 设置线程的中断标识位为true
thread.cancel();
}
private static class ChildThread extends Thread {
private volatile boolean isCancel;
@Override
public void run() {
super.run();
// 判断isCancel标识位是否为true,
// 如果isCancel标识位不为true则会一直调用doing,否则会被中断
while (!isCancel) {
doing();
}
}
private void doing() {
System.out.print("执行任务");
}
public void cancel() {
isCancel = true;
}
}
其实以上两种方式的原理相同,都是通过一个标识位来判断当前线程是否应该被中断。
总结
虽然线程的stop方法被弃用了,但是却有中断线程这种替代方案。线程的中断标识位只不过是一个普通的标识位,开发者可以根据线程的中断标识位来决定是否应该中断线程,可以根据实际的项目需求使用线程的中断标识位,如果是非常重要的线程则不理会即可。但是对于一个处于阻塞状态的线程,若线程在检测线程的中断标识位时发现线程的中断标识位为true时,抛出的InterruptedException异常被捕获后一定要做处理。