在 Android 开发中,多线程编程是处理耗时操作(如网络请求、数据库读写、复杂计算)的基础。Thread 和 Runnable 是 Java 中最原始、最核心的线程机制,Android 完全继承了这套体系。本文将详细讲解它们的概念、用法、区别以及在 Android 中的注意事项,不涉及协程相关内容。
1. 基本概念
1.1 Thread(线程)
-
Thread是 Java 中代表一个独立执行路径的类。 -
每个
Thread对象对应操作系统中的一个实际线程(或映射到内核线程,具体取决于 JVM 实现)。 -
线程拥有自己的程序计数器、栈和局部变量,但共享进程内的堆内存和静态变量。
-
在 Android 中,主线程(UI 线程)也是一个
Thread实例,负责处理界面绘制和用户交互。
1.2 Runnable(任务)
-
Runnable是一个函数式接口,只声明了一个方法void run()。 -
它本身不是线程,只是一个可以被线程执行的任务描述。
-
实现了
Runnable的类可以被提交给Thread对象执行,也可以被线程池(如ExecutorService)执行。 -
Android 中的
Handler.post(Runnable)等也利用了Runnable作为任务载体。
2. 创建线程的两种方式
2.1 继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
// 在线程中执行的代码
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
// 使用
MyThread t = new MyThread();
t.start(); // 启动新线程
2.2 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
// 任务代码
System.out.println("Runnable running: " + Thread.currentThread().getName());
}
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start();
2.3 使用匿名内部类或 Lambda(Java 8+)
// 匿名内部类
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 代码
}
});
t1.start();
// Lambda 表达式(Runnable 是函数式接口)
Thread t2 = new Thread(() -> {
// 代码
});
t2.start();
3. start() 与 run() 的区别
这是新手最容易混淆的地方:
| 方法 | 作用 | 是否创建新线程 |
|---|---|---|
start() |
启动线程,JVM 会调用该线程的 run() 方法,并在新线程中执行 |
是 |
run() |
仅仅是普通方法调用,在当前线程中执行 run() 方法体 |
否 |
Thread t = new Thread(() -> {
System.out.println("线程名: " + Thread.currentThread().getName());
});
t.run(); // 输出:线程名: main (因为仍在主线程执行)
t.start(); // 输出:线程名: Thread-0 (新建的线程)
重要: 要真正开启一个新线程,必须调用 start(),而不是 run()。
4. Thread 的常用方法和生命周期
4.1 常用方法
-
start():启动线程 -
run():线程体 -
sleep(long millis):静态方法,让当前线程休眠指定毫秒,不会释放锁 -
join():等待该线程终止 -
interrupt():中断线程(设置中断标志,并不会立即停止线程) -
isAlive():判断线程是否存活 -
setPriority(int):设置线程优先级(1~10,默认5) -
setDaemon(boolean):设置是否为守护线程(当所有非守护线程结束时,JVM 退出,不论守护线程是否执行完毕) -
setName(String)/getName():设置/获取线程名
4.2 生命周期状态
线程在 Thread.State 枚举中定义了六种状态:
-
NEW :创建但尚未调用
start() -
RUNNABLE:可运行状态,包括正在运行和等待 CPU 调度
-
BLOCKED:阻塞,等待获取锁
-
WAITING :无限等待,通过
wait()、join()等进入 -
TIMED_WAITING :限时等待,通过
sleep()、wait(timeout)等进入 -
TERMINATED:线程执行完毕
状态转换图略,理解即可。
5. Thread 与 Runnable 的关系与区别
5.1 关系
-
Thread是线程的抽象,负责创建和控制线程。 -
Runnable是任务的抽象,负责定义要执行的具体逻辑。 -
Thread内部可以持有一个Runnable对象作为任务(通过构造方法传入)。 -
Thread类本身实现了Runnable接口,所以也可以把Thread当作一个任务交给另一个Thread执行。
5.2 区别
| 维度 | 继承 Thread | 实现 Runnable |
|---|---|---|
| 类继承限制 | Java 单继承,若继承 Thread 则无法再继承其他类 | 可以实现多个接口,不占用继承名额 |
| 资源共享 | 每个线程是独立的 Thread 子类实例,资源不易共享(除非用 static) | 同一个 Runnable 实例可以被多个 Thread 共享,更适合资源共享场景 |
| 代码组织 | 线程和任务绑定在一起,耦合度高 | 任务与线程控制分离,更符合单一职责原则 |
| 灵活性 | 较差,一旦继承 Thread 就不能更改任务逻辑 | 更灵活,同一个 Runnable 可以提交给不同线程或线程池 |
| 适用场景 | 适合简单的、独立的线程,不需要共享太多数据 | 适合需要将任务与线程分离、多个线程执行相同任务的情况 |
结论: 一般推荐实现 Runnable 接口,因为可以避免单继承限制,并且任务与线程控制分离,代码结构更清晰。
6. 在 Android 中使用 Thread 的注意事项
6.1 主线程与子线程
-
Android 的主线程(UI 线程)负责处理界面更新、用户交互等,不能执行耗时操作,否则会导致 ANR(应用无响应)。
-
耗时操作(网络、数据库、大量计算)必须在子线程中执行。
-
使用
Thread或Runnable可以轻松创建子线程。
new Thread(() -> {
// 耗时操作,比如下载文件
String data = downloadData();
// 不能直接更新 UI,会抛出 CalledFromWrongThreadException
// textView.setText(data); // 错误!
}).start();
6.2 更新 UI 的方式
子线程中不能直接更新 UI,必须通过以下方式:
-
Activity.runOnUiThread(Runnable) -
View.post(Runnable) -
Handler(主线程 Handler 发送消息或 Runnable)
new Thread(() -> {
String data = downloadData();
// 方式一:runOnUiThread
runOnUiThread(() -> textView.setText(data));
// 方式二:View.post
textView.post(() -> textView.setText(data));
// 方式三:Handler
mainHandler.post(() -> textView.setText(data));
}).start();
6.3 线程泄漏
-
在 Activity 或 Fragment 中启动线程,如果线程持有 Activity 的引用,并且线程长时间运行,可能导致 Activity 无法被回收,造成内存泄漏。
-
解决方法:在
onDestroy()中停止线程,或者使用静态内部类 + 弱引用,更推荐使用线程池或AsyncTask(已废弃)或协程(此处不讨论)。
6.4 线程数量控制
-
频繁创建和销毁线程开销较大,且大量线程会消耗系统资源。
-
在 Android 中,对于多任务场景,应优先使用线程池(
ExecutorService),而不是无限创建Thread对象。
7. 示例:使用 Thread 和 Runnable 实现一个简单的异步任务
以下代码演示如何在 Android 中结合 Thread、Runnable 和 Handler 完成网络请求并更新 UI。
public class MainActivity extends Activity {
private TextView textView;
private Handler mainHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.text_view);
// 创建 Runnable 任务
Runnable fetchDataTask = new Runnable() {
@Override
public void run() {
// 模拟耗时操作
String result = fetchDataFromNetwork();
// 切回主线程更新 UI
mainHandler.post(new Runnable() {
@Override
public void run() {
textView.setText(result);
}
});
}
};
// 创建并启动线程
Thread workerThread = new Thread(fetchDataTask);
workerThread.start();
}
private String fetchDataFromNetwork() {
try {
Thread.sleep(2000); // 模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
return "数据加载完成";
}
}
8. 总结
-
Thread是线程,Runnable是任务。推荐使用实现Runnable的方式将任务与线程分离。 -
调用
start()才会启动新线程,直接调用run()只是在当前线程执行方法体。 -
在 Android 中,所有耗时操作必须在子线程,但 UI 更新必须在主线程。
-
注意线程的生命周期和资源管理,避免内存泄漏和过度创建线程。
-
掌握
Thread和Runnable是理解 Android 多线程和后续学习 Handler、线程池、协程的基础。
Runnable 本身不能独立运行,它只是一个包含 run() 方法的接口,用来描述一段需要执行的代码。它必须被某个执行环境调用其 run() 方法才能真正执行,而这个执行环境本质上是一个线程(或线程池中的某个线程)。
最常见的执行方式有:
-
直接使用
Thread类new Thread(runnable).start();这是最基础的方式,
Thread会创建一个新线程,并在该线程中调用Runnable的run()方法。 -
使用
ExecutorService(线程池)ExecutorService executor = Executors.newFixedThreadPool(3); executor.execute(runnable); // 线程池中的某个线程会执行它线程池内部也是由多个
Thread组成的,所以本质上还是由线程执行。 -
通过
Handler投递到消息队列Handler handler = new Handler(Looper.getMainLooper()); handler.post(runnable); // 主线程会在处理消息时执行它这里
Runnable会被主线程(也是一个Thread)在某个时刻调用run(),但它并不是直接新建一个Thread去执行。
所以更准确的说法是:
Runnable 不能独立运行,它必须被某个线程(或线程池、Handler 等执行机制)调用其 run() 方法才能执行。
虽然不一定直接通过 new Thread(runnable),但执行它的最终载体始终是某个线程。