Thread类的用法(1)--Thread类的基本用法
Java中创建线程的方式
在Java中,创建并启动一个线程有多种方式,下面通过5个代码案例逐一讲解。这些案例都实现了同一个功能:主线程每隔1秒打印"hello main",子线程每隔1秒打印"hello thread"。
方式一:继承Thread类,重写run方法
java
package thread;
class MyThread extends Thread{
//继承thread类,重写run方法
//run方法代表线程的入口,线程执行起来后,要执行什么样的逻辑
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
//使用这个线程
MyThread t = new MyThread();
//启用这个线程
t.start();
while(true) {
System.out.println("hello,world");
Thread.sleep(1000);
}
}
}
解释 :这是最传统的写法。我们定义一个类MyThread,让它继承Thread类,并重写run()方法------run()就是线程的入口方法,线程启动后会执行里面的逻辑。在main方法中,先创建MyThread实例,然后调用start()方法启动线程(注意:直接调用run()不会启动新线程,只是普通方法调用)。启动后,主线程和子线程会并发执行,各自打印自己的内容。
优点 :代码直观,适合简单的线程任务。
缺点 :Java是单继承,继承了Thread就不能再继承其他类,扩展性差。
代码运行结果如下:
方式二:实现Runnable接口,实现run方法
java
package thread;
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo2 {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
// runnable.start(); // 错误!Runnable没有start方法
Thread t = new Thread(runnable);
t.start();
while (true) {
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
解释 :这种方式将"任务"和"线程"分离开。MyRunnable只负责定义要执行的任务(实现run()方法),它本身不是线程。要启动这个任务,需要创建一个Thread对象,并把MyRunnable实例传进去,然后调用Thread的start()方法。注意注释中写的runnable.start()是错误的------Runnable接口没有start()方法。
优点:Java支持实现多个接口,不影响类的继承体系,更灵活。适合多个线程共享同一个任务实例的场景。

方式三:使用匿名内部类,继承Thread类
java
package thread;
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread() {
// 子类中的属性和方法.
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
while (true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
解释 :这是方式一的简化版。我们不再单独定义一个MyThread类,而是直接在new Thread()后面跟一对大括号{},在里面重写run()方法。这个{}就是一个匿名内部类,它相当于创建了一个Thread的子类,但类名是匿名的。这种方式适合线程逻辑比较简单、只在当前代码中使用一次的场景。
优点 :代码更紧凑,不需要额外定义类。
缺点:如果线程逻辑复杂,匿名内部类会让代码可读性变差。
代码运行结果如下:

方式四:使用匿名内部类,实现Runnable接口
java
package thread;
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t = new Thread(runnable);
t.start();
while (true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
解释 :这是方式二的简化版。我们不再单独定义MyRunnable类,而是用匿名内部类的方式直接创建Runnable接口的实现。new Runnable() { ... }创建了一个实现了Runnable接口的匿名对象,然后把这个对象传给Thread的构造方法。这种方式结合了方式二的灵活性和匿名内部类的简洁性,是实际开发中非常常见的写法。
优点:既保留了实现接口的灵活性,又省去了单独定义类的步骤。
代码运行结果如下:

方式五:使用Lambda表达式
java
package thread;
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while (true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
解释 :这是Java 8引入的Lambda表达式写法,是方式四的进一步简化。Runnable接口是一个函数式接口(只有一个抽象方法run()),所以可以用Lambda表达式() -> { ... }来直接表示它的实现。()对应run()方法的参数(无参),->后面的大括号就是run()的方法体。这是目前最简洁、最推荐的写法。
优点 :代码最简洁,可读性高,是Java 8及以后版本的首选写法。
缺点:初学者需要先理解Lambda表达式的语法。
代码运行结果如下:

Lambda表达式中的变量捕获:必须是事实常量
在使用Lambda表达式时,有一个非常重要的约束:Lambda表达式中引用的局部变量必须是事实常量(effectively final)。也就是说,这个变量一旦被赋值后,就不能再被修改------无论是在Lambda表达式内部还是外部,否则编译器会直接报错。
来看一个错误的例子:
java
public class DemoError {
public static void main(String[] args) {
int count = 0; // 这个变量后续被修改了
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello thread, count = " + count);
// count++; // 这里也会报错,不能在Lambda内部修改
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
count = 10; // ❌ 编译错误!count被重新赋值,不再是事实常量
t.start();
}
}
上面的代码会编译失败,错误信息类似于:Variable used in lambda expression should be effectively final。因为count在声明后被重新赋值为10,它就不再是事实常量了。
正确的做法 :如果需要在Lambda中使用一个值,要么声明时就加上final关键字,要么确保变量在初始化后不再被修改:
java
public class DemoCorrect {
public static void main(String[] args) {
int count = 0; // 初始化后不再修改,是事实常量
// count = 10; // 如果取消注释这行,就会报错
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello thread, count = " + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
为什么要有这个约束?
这涉及到Java的内存模型。Lambda表达式本质上是一个匿名函数,它可能会在另一个线程中执行。如果Lambda表达式引用的局部变量可以被随意修改,那么多个线程同时访问这个变量时就会出现线程安全问题。Java通过要求变量是事实常量,保证了Lambda表达式内部看到的变量值是一致的、不会发生并发修改的混乱。
注意 :这个约束只针对局部变量 。如果Lambda表达式访问的是类的成员变量(实例变量或静态变量),则没有"事实常量"这个编译期限制------你可以在Lambda内部或外部随意修改成员变量的值,编译器不会报错。
但这并不意味着在多线程环境下访问成员变量就是安全的。恰恰相反,这里隐藏着更大的风险:
1. 成员变量可以被随意修改,导致线程安全问题
java
public class UnsafeCounter {
private int count = 0; // 成员变量,不是事实常量
public void startThread() {
Thread t = new Thread(() -> {
while (true) {
count++; // ✅ 编译器不会报错,但存在线程安全问题
System.out.println("count = " + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
上面的代码可以正常编译,但count++不是原子操作,多个线程同时访问时会出现数据不一致的问题。你需要自己用synchronized、volatile或AtomicInteger等机制来保证线程安全。
2. 对象被销毁的风险
Lambda表达式捕获的是this引用(即当前对象的引用),而不是成员变量的副本。这意味着Lambda表达式内部持有的是对外部对象的隐式引用。如果这个外部对象在其他地方被置为null或超出了作用域,而Lambda表达式所在的线程还在运行,就可能出现意想不到的问题:
java
public class DangerousExample {
private String message = "hello";
public void startThread() {
Thread t = new Thread(() -> {
try {
Thread.sleep(5000); // 等待5秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 5秒后,外部对象可能已经被销毁
System.out.println(message); // 可能抛出NullPointerException
});
t.start();
}
public static void main(String[] args) {
new DangerousExample().startThread();
// main方法执行完毕,DangerousExample对象可能被GC回收
// 但Lambda表达式仍然持有对它的引用
System.gc(); // 强制GC,可能导致对象被回收
}
}
虽然Lambda表达式持有外部对象的引用可以防止对象被GC回收(只要Lambda还在运行,外部对象就不会被回收),但如果你在Lambda执行期间修改了成员变量的值,或者外部对象的状态发生了变化,Lambda内部看到的数据可能已经不是预期的数据了。
总结:Lambda表达式访问成员变量时,编译器不会强制要求"事实常量",但这并不意味着安全。你需要自己负责:
- 使用同步机制保证成员变量的线程安全
- 注意Lambda对外部对象的隐式引用,避免状态不一致
- 如果可能,优先将需要的值作为局部变量捕获(利用事实常量的约束),而不是直接访问成员变量
总结对比
继承Thread vs 实现Runnable:核心区别
理解这两种方式的本质区别,对掌握Java多线程非常重要。
1. 本质不同:一个是类,一个是接口
继承Thread,意味着你创建了一个Thread的子类,这个子类本身就是一个线程对象。你可以直接调用start()方法启动它:
java
MyThread t = new MyThread();
t.start(); // 直接调用,因为MyThread是Thread的子类
而实现Runnable,只是定义了一个"任务"------你规定了线程要执行的逻辑(run()方法),但这个任务本身并不是线程。Runnable接口中只有一个run()方法,没有start()方法,所以你不能这样写:
java
MyRunnable runnable = new MyRunnable();
runnable.start(); // ❌ 编译错误!Runnable接口中没有start()方法
正确的做法是把Runnable实例传给Thread的构造方法,由Thread来启动:
java
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start(); // ✅ 由Thread对象来启动线程
2. 继承体系的影响
Java是单继承的。如果继承了Thread,就不能再继承其他类,这限制了类的扩展性。而实现Runnable接口则没有这个限制,你的类还可以继承其他类或实现更多接口。
3. 任务与线程的分离程度
实现Runnable将"要执行的任务"和"执行任务的线程"解耦了。同一个Runnable实例可以传给多个Thread对象,实现多个线程共享同一份任务逻辑。而继承Thread的方式,每个线程实例都绑定了一个特定的任务类,复用性较差。
4. 适用场景
- 继承Thread:适合简单的、不需要再继承其他类的场景,代码直观,适合初学者理解线程的基本概念。
- 实现Runnable:更灵活,是实际开发中更推荐的方式,尤其适合需要共享任务实例或类已有继承关系的场景。
| 方式 | 核心思想 | 代码量 | 推荐场景 |
|---|---|---|---|
| 继承Thread | 类继承 | 较多 | 简单任务,不关心扩展性 |
| 实现Runnable | 接口实现 | 较多 | 需要灵活扩展,或共享任务 |
| 匿名内部类-继承Thread | 匿名子类 | 中等 | 一次性使用的线程 |
| 匿名内部类-实现Runnable | 匿名实现 | 中等 | 一次性使用的任务,兼顾灵活性 |
| Lambda表达式 | 函数式编程 | 最少 | 现代Java首选 |
从方式一到方式五,代码越来越简洁,体现了Java语言从面向对象到函数式编程的演进趋势。在实际开发中,方式五(Lambda表达式) 是最推荐的做法,但理解前四种写法有助于你深入理解Java线程的底层机制。
start()方法和run()方法的区别
很多初学者容易混淆start()和run()这两个方法,下面通过代码和原理来彻底讲清楚它们的区别。
1. 本质区别:启动线程 vs 定义任务
start():它的作用是启动一个新线程 。调用start()后,JVM会创建一个新的线程,并让这个新线程去执行run()方法中的代码。start()方法会立即返回,不会阻塞调用它的线程。run():它的作用是定义线程要执行的任务逻辑 。它只是一个普通的方法,里面封装了线程启动后要执行的代码。直接调用run()不会创建新线程,它会在当前线程中像普通方法一样被顺序执行。
2. 代码演示:直接调用run() vs 调用start()
来看一个对比示例,直观感受两者的区别:
java
public class StartVsRunDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName());
});
System.out.println("=== 直接调用 run() ===");
t.run(); // 不会启动新线程,在main线程中执行
System.out.println("=== 调用 start() ===");
t.start(); // 启动新线程,在新线程中执行
}
}
运行结果如下:
=== 直接调用 run() ===
当前线程:main ← 注意:是main线程在执行
=== 调用 start() ===
当前线程:Thread-0 ← 新创建的线程在执行
从输出可以清楚看到:
- 直接调用
t.run()时,run()方法中的代码是在**主线程(main)**中执行的,没有创建新线程。 - 调用
t.start()时,JVM创建了一个名为Thread-0的新线程,由这个新线程去执行run()方法中的代码。
3. 为什么不能直接调用run()来启动线程?
因为run()只是一个普通的方法,它不具备创建新线程的能力。创建新线程是start()方法的职责 。start()方法内部会做以下几件事:
- 检查线程状态 :确保线程没有被重复启动(一个线程只能调用一次
start(),重复调用会抛出IllegalThreadStateException)。 - 加入线程组:将当前线程加入线程组中管理。
- 调用本地方法
start0():这是一个JVM层面的本地方法(native method),它会向操作系统申请创建一个新的线程,并让这个新线程去执行run()方法。
而run()方法只是一个普通的实例方法,直接调用它就是在当前线程中执行,不会触发上述任何线程创建流程。
4. 一个线程只能调用一次start()
java
Thread t = new Thread(() -> System.out.println("hello"));
t.start();
t.start(); // ❌ 抛出 IllegalThreadStateException
第二次调用start()会抛出异常,因为线程的状态已经从NEW变为RUNNABLE,不允许再次启动。而run()方法可以反复调用,因为它只是一个普通方法。
5. 总结对比
| 对比项 | start() |
run() |
|---|---|---|
| 作用 | 启动一个新线程 | 定义线程要执行的任务 |
| 是否创建新线程 | ✅ 是,由JVM创建 | ❌ 否,在当前线程中执行 |
| 调用次数限制 | 只能调用一次 | 可以多次调用 |
| 调用方式 | 由Thread对象调用 |
由Thread对象调用,或由Runnable实例调用 |
| 底层实现 | 调用JVM本地方法start0() |
普通Java方法 |
一句话总结 :start()负责创建并启动线程 ,run()负责规定线程启动后要做什么 。永远不要直接调用run()来"启动"线程------那只是普通的方法调用,不会产生多线程效果。
Thread类的基本用法
前面我们学习了创建线程的5种方式,以及start()和run()的区别。接下来,我们来系统学习Thread类的一些基本用法,包括线程的创建、中断、等待、休眠以及获取线程实例等操作。
1. 线程创建
创建线程最核心的一步就是实例化一个Thread对象 ,并调用它的start()方法启动。前面我们已经通过5种方式演示了如何创建线程,这里不再赘述。简单回顾一下核心流程:
java
// 1. 定义任务(Runnable)
Runnable task = () -> {
System.out.println("线程执行中...");
};
// 2. 创建Thread对象,传入任务
Thread t = new Thread(task);
// 3. 启动线程
t.start();
2. 获取线程实例与判断存活状态
在Java中,可以通过Thread.currentThread()获取当前正在执行的线程实例。而isAlive()方法可以判断一个线程是否还存活(即是否已经启动且尚未终止)。
java
package thread;
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for(int i = 0; i < 3; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 获取 t 的存活状态
System.out.println(t.isAlive());
t.start();
Thread.sleep(1000);
// 此时 t 还在运行中
System.out.println(t.isAlive());
Thread.sleep(4000);
// 此时 t 已经结束了
System.out.println(t.isAlive());
}
}
解释:
- 在
t.start()之前调用t.isAlive(),返回false,因为线程还没有启动。 - 启动后1秒,线程还在执行循环,
isAlive()返回true。 - 再等4秒,线程的循环已经执行完毕(3次循环,每次sleep 1秒,总共约3秒),线程终止,
isAlive()返回false。
注意 :isAlive()返回false只表示线程已经终止,不代表线程对象被回收。线程对象仍然存在,只是不能再被重新启动。
3. 线程中断
线程中断是一种协作式的机制------一个线程可以向另一个线程发送"中断请求",但目标线程是否响应、何时响应,完全由目标线程自己决定。Java提供了两种常见的中断方式:
方式一:使用标志位(自定义变量)
通过一个共享的boolean变量来控制线程的终止。这种方式简单直观,但需要注意volatile关键字保证变量的可见性。
java
package thread;
import java.util.Scanner;
public class Demo9 {
private static volatile boolean running = true;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (running) {
System.out.println("hello thread");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("thread 结束");
});
t.start();
Scanner scanner = new Scanner(System.in);
System.out.println("输入任意内容, 控制 t 线程结束: ");
scanner.next();
running = false; // 修改标志位,t线程检测到后退出循环
}
}
解释:
running是一个被volatile修饰的共享变量,保证了多个线程之间的可见性。- 子线程在
while (running)循环中不断执行任务,当主线程将running置为false时,子线程检测到条件不满足,退出循环,线程自然结束。 - 这种方式需要我们自己定义标志位,适合简单的场景。
方式二:使用interrupt()方法(官方推荐)
Thread类内置了中断机制,通过interrupt()方法发送中断请求,目标线程通过isInterrupted()或Thread.interrupted()来检测中断状态。
java
package thread;
import java.util.Scanner;
public class Demo10 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 1. 立即终止线程
// break;
// 2. 稍后处理
// 先执行一些其他操作, 比如释放资源收尾操作, 然后再 break
// break;
// 3. 不处理, 忽略了终止请求.
continue;
}
}
System.out.println("线程 t 终止");
});
t.start();
System.out.println("输入任意内容, 终止 t 线程");
Scanner scanner = new Scanner(System.in);
scanner.next();
// 终止 t 线程
t.interrupt();
}
}
解释:
Thread.currentThread().isInterrupted():检测当前线程是否被中断。如果返回true,说明有其他线程调用了当前线程的interrupt()方法。- 当线程处于
sleep()、wait()、join()等阻塞状态 时,如果其他线程调用了该线程的interrupt()方法,会抛出InterruptedException异常。此时线程的中断标志位会被清除(置为false),我们需要在catch块中决定如何处理:- 立即终止 :直接
break退出循环。 - 稍后处理 :先执行一些收尾操作(如释放资源),再
break。 - 忽略请求 :
continue继续执行,不响应中断。
- 立即终止 :直接
两种方式的对比:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 自定义标志位 | 简单直观,容易理解 | 需要自己处理可见性(volatile),无法中断sleep等阻塞状态 |
interrupt() |
官方API,能中断阻塞状态,更规范 | 需要处理InterruptedException,逻辑稍复杂 |
4. 线程休眠
Thread.sleep(long millis)是一个静态方法,让当前正在执行的线程暂停执行指定的毫秒数 ,进入TIMED_WAITING状态。休眠期间,线程不会占用CPU资源。
java
try {
Thread.sleep(1000); // 当前线程休眠1秒
} catch (InterruptedException e) {
// 如果在休眠期间被中断,会抛出InterruptedException
e.printStackTrace();
}
注意事项:
sleep()会抛出InterruptedException,必须捕获或声明抛出。sleep()不会释放锁(如果线程持有锁,休眠期间其他线程仍然无法获取该锁)。- 休眠时间不保证绝对精确,受操作系统调度影响。
5. 线程等待(join)
join()方法让当前线程等待目标线程执行完毕 。如果线程A中调用了t.join(),那么线程A会阻塞,直到线程t执行完毕(或超时)才继续执行。
java
Thread t = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("子线程执行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
t.join(); // 主线程等待t线程执行完毕
System.out.println("子线程已结束,主线程继续执行");
解释:
- 主线程调用
t.join()后,会进入WAITING状态,直到t线程执行完毕。 join()也提供了带超时参数的版本:join(long millis),表示最多等待指定的毫秒数,超时后无论目标线程是否结束,当前线程都会继续执行。join()同样会抛出InterruptedException,需要处理。
6. 线程的不可重复启动
一个线程对象只能调用一次start()方法 。如果重复调用,会抛出IllegalThreadStateException。
java
package thread;
public class Demo8 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello thread");
});
t.start();
System.out.println("线程第一次启动");
t.start(); // ❌ 抛出 IllegalThreadStateException
System.out.println("线程第二次启动");
}
}
解释:
- 第一次调用
t.start()成功启动线程,线程状态从NEW变为RUNNABLE。 - 第二次调用
t.start()时,线程已经不是NEW状态,JVM会抛出IllegalThreadStateException。 - 如果需要再次执行相同的任务,必须重新创建一个新的
Thread对象。
总结
| 方法 | 作用 | 说明 |
|---|---|---|
start() |
启动线程 | 只能调用一次,重复调用抛异常 |
isAlive() |
判断线程是否存活 | 线程启动后到终止前返回true |
sleep(ms) |
让当前线程休眠 | 静态方法,不释放锁 |
join() |
等待目标线程结束 | 当前线程阻塞,直到目标线程执行完毕 |
interrupt() |
发送中断请求 | 协作式中断,目标线程自行决定是否响应 |
isInterrupted() |
检测中断状态 | 不会清除中断标志位 |
Thread.currentThread() |
获取当前线程实例 | 静态方法,常用于Lambda内部获取当前线程引用 |
这些是Thread类最常用的基本用法,掌握它们之后,你就能灵活地控制线程的创建、执行、休眠、等待和终止了。
