1. Thread 类:在 Java 中的第一个线程
在 Java 中,每个执行线程都由 Thread 类的对象表示。这个类就像乐队的指挥:负责管理线程的启动、停止和生命周期。
要启动一个线程,需要:
- 创建一个 Thread(或其子类)的对象。
- 调用该对象的 start() 方法。
让我们通过实践来理解。
示例 1. 继承 Thread
最直接的创建线程方式是:让你的类继承 Thread,并重写它的 run() 方法。你在 run() 中写的内容会在单独的线程中执行。
// 通过继承的简单线程
public class HelloThread extends Thread {
@Override
public void run() {
System.out.println("来自线程的问候!我的名字是:" + getName());
}
}
public class Main {
public static void main(String[] args) {
HelloThread thread = new HelloThread(); // 创建线程对象
thread.start(); // 启动线程
System.out.println("主线程结束运行。");
}
}
实际上发生了什么?
当你调用 thread.start() 时,Java 会创建一个新线程,并在该新线程中启动 run() 方法。同时,主线程(从 main 开始的那个)不会等待,而是并行继续执行。
重要提醒:不要把 start() 和直接调用 run() 混淆。如果写成 thread.run(),不会创建新线程——这只是一个普通方法,会在当前线程中执行。真正的多线程只会从 start() 开始。
什么是 getName()?
getName() 方法会返回线程的名称。默认情况下,Java 会给线程起名为 "Thread-0"、"Thread-1" 等等。这对于调试很有帮助。
2. Runnable 接口:大多数情况下的更佳方式
Java 追求灵活性。Thread 类已经继承自 Object,而 Java 不支持类的多重继承。如果你的类继承了 Thread,就不能再继承其他类(比如你有一个 Car 类,想把它变成线程,就必须二选一)。
解决方案:把线程逻辑放到一个实现了 Runnable 接口的独立对象中。该接口只有一个方法 run()。然后创建一个 Thread 对象,把你的 Runnable 传给它——再启动线程。
示例 2. 带 Runnable 的类
// 实现 Runnable 的类
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("来自 Runnable 的问候!线程:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Runnable runnable = new HelloRunnable();
Thread thread = new Thread(runnable); // 将 Runnable 传入 Thread
thread.start();
System.out.println("主线程结束运行。");
}
}
这里我们创建了一个实现 Runnable 接口的类 HelloRunnable。在 main 方法中创建该类的对象,并把它传给 Thread 的构造器。之后通过调用 start() 来启动线程。
值得单独一提的是 Thread.currentThread() 方法。它是一个静态方法,用来获取当前线程对象,从而知道我们的代码究竟在哪个线程中执行。
为什么这样更好?
- 你的类可以继承任何其他类(而不只是 Thread)。
- 可以复用同一个 Runnable,在多个线程中运行。
- 代码更灵活、更清晰。
3. 语法:匿名类与 lambda 表达式
Java 在不断进化!自 Java 8 之后,我们可以写得更简洁、更方便。
示例 3. 匿名类
有时你不想为只用一次的类创建单独的文件。可以在使用处直接声明——这就是匿名类。
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名 Runnable!线程:" + Thread.currentThread().getName());
}
});
thread.start();
System.out.println("主线程结束运行。");
}
}
示例 4. Lambda 表达式 (Java 8+)
Runnable 接口是函数式接口(只有一个抽象方法)。因此可以使用 lambda 表达式:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Lambda 线程!线程:" + Thread.currentThread().getName());
});
thread.start();
System.out.println("主线程结束运行。");
}
}
小结:
Runnable 可以“就地”实现,而 lambda 表达式提供了 run() 方法的实现。
4. 启动多个线程
你可以启动任意多个线程(几乎——这取决于计算机的资源)。
示例 5. 启动多个线程
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
int threadNumber = i; // 对于 lambda,必须复制变量!
Thread thread = new Thread(() -> {
System.out.println("线程 #" + threadNumber + ":你好!");
});
thread.start();
}
System.out.println("主线程结束运行。");
}
}
输出可能不同!
线程并行运行——谁先输出取决于操作系统和线程调度器。
5. 线程的生命周期
理解线程如何运行,有助于调试和正确使用线程。
线程的主要状态
- NEW — 线程已创建,但尚未启动(new Thread(...))。
- RUNNABLE — 线程已启动(start()),可能正在执行。
- TERMINATED — 线程已结束(run() 方法执行完毕)。
调用 start() 与 run() 时发生了什么
- start() — 创建一个新线程,并在该新线程中调用它的 run() 方法。
- run() — 普通方法;如果直接调用,它会在当前线程中执行(不会创建新线程)。
示例:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("来自线程的 run():" + Thread.currentThread().getName()));
thread.run(); // 在主线程中调用!
thread.start(); // 在单独的线程中调用!
}
}
6. 实践:做一个简单的多线程应用
让我们继续完善我们的教学示例——假设我们在编写一个简单的日志系统,每个线程写出自己的消息。
public class LoggerTask implements Runnable {
private final String message;
public LoggerTask(String message) {
this.message = message;
}
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "] 日志:" + message);
}
}
public class Main {
public static void main(String[] args) {
Thread logger1 = new Thread(new LoggerTask("系统启动"));
Thread logger2 = new Thread(new LoggerTask("加载数据"));
Thread logger3 = new Thread(new LoggerTask("处理请求"));
logger1.start();
logger2.start();
logger3.start();
System.out.println("主线程结束运行。");
}
}
发生了什么?
三个线程并行地向控制台写出各自的消息。消息的顺序不保证——这正是多线程的特性。
7. 一些有用的细节
表格:创建线程方式的比较
| 方式 | 灵活性 | 可复用性 | 是否推荐? |
|---|---|---|---|
| 继承 Thread | 低 | 否 | 仅用于简单/教学示例 |
| 实现 Runnable | 高 | 是 | 是 |
| 匿名类 | 中等 | 否 | 用于一次性的任务 |
| Lambda 表达式 | 高 | 否 | 是(Java 8+) |
示意图:线程启动如何工作
+----------------------+
| main (主线程) |
+----------------------+
|
v
+----------------------+
| Thread thread = ... |
+----------------------+
|
v
+----------------------+
| thread.start() | ← 线程“开始运行”
+----------------------+
|
v
+----------------------+
| run() 在新线程中 |
+----------------------+
|
v
+----------------------+
| 线程已结束 |
+----------------------+
8. 启动线程的常见错误
错误 1:调用 run() 而不是 start()。
新手非常常见的错误是直接调用线程对象的 run() 方法。此时不会创建新线程,方法只会在当前(主)线程中执行。正确做法:启动线程时始终使用 start()。
错误 2:重复启动同一个线程。
不能对同一个 Thread 对象多次调用 start()——这会抛出 IllegalThreadStateException。如果需要再次运行任务,请创建一个新的 Thread 对象。
错误 3:未同步就修改共享变量。
如果多个线程访问同一组变量,结果可能出乎意料(竞态条件)。关于同步我们会稍后讨论,但目前请务必小心。
错误 4:没有等待线程结束。
如果必须等待某个线程完成,请使用 join()。否则主线程可能会先于子线程结束。
错误 5:把线程名和类名混淆。
getName() 方法返回的是线程名,而不是类名。为了调试,最好通过构造器或 setName() 显式设置线程名。
GO TO FULL VERSION