线程的创建与启动:从基础到实战
各位同学,今天我们来聊聊多线程编程的第一步——线程的创建与启动。说实话,这个主题看起来简单,但我在面试中见过不少人在细节上翻车。我自己刚入行时也踩过坑,比如把线程当普通对象调用方法,结果程序跑得莫名其妙。
线程的创建,说白了就是告诉操作系统:「嘿,我要开一条新的执行路径」。Java里提供了几种方式,每种都有它的适用场景。我们一个一个来看。
继承Thread类:最直接的方式
这种方式最简单,你写一个类继承Thread,重写run()方法,然后调用start()启动。嗯,这里要注意:千万别直接调run(),否则它就在当前线程里跑,不会创建新线程。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
}
// 启动
MyThread t = new MyThread();
t.start(); // 正确
// t.run(); // 错误!不会启动新线程
我曾经犯过的错:刚学多线程时,我写了个循环调用t.run(),以为能并行处理任务。结果发现所有任务都在主线程里串行执行,性能一点没提升。后来才明白,start()才是让JVM创建新线程的关键。
继承方式的缺点也很明显:Java是单继承,你继承了Thread就不能继承别的类了。所以,我个人习惯更推荐下面这种方式。
实现Runnable接口:更灵活的选择
实现Runnable接口,把任务逻辑写在run()里,然后传给Thread构造器。这样你的类还可以继承其他类,代码也更清晰——任务和线程分离了。
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("任务执行中:" + Thread.currentThread().getName());
}
}
// 启动
Thread t = new Thread(new MyTask());
t.start();
你想想看,这种方式是不是更符合面向对象的设计原则?任务本身是一个独立的对象,线程只是它的执行载体。我在项目中处理大量并发任务时,基本都是用Runnable,配合线程池使用非常方便。
使用Callable与Future:带返回值的线程
Runnable有个局限——它不能返回结果,也不能抛异常。如果你需要线程计算完返回一个值,比如查询数据库后返回结果集,那就得用Callable了。
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 模拟耗时计算
Thread.sleep(1000);
return 42;
}
}
// 使用线程池提交
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable());
// 获取结果(会阻塞直到计算完成)
Integer result = future.get();
System.out.println("计算结果:" + result);
executor.shutdown();
核心要点:Future.get()是阻塞的,它会等待线程执行完毕。如果线程一直不结束,你的主线程也会卡住。我在做爬虫项目时,就遇到过某个请求超时导致整个程序挂起的情况。后来用future.get(5, TimeUnit.SECONDS)设置了超时时间,问题就解决了。
Callable和Future的组合,说白了就是「异步任务+结果获取」的经典模式。你提交任务,得到一个Future凭证,等需要结果时再通过凭证去取。这比Runnable灵活多了。
线程的Daemon属性设置
Daemon线程,中文叫守护线程。它的特点是:当所有非守护线程结束时,JVM会自动退出,不管守护线程是否还在运行。
举个例子,垃圾回收线程就是守护线程。你写一个后台日志收集线程,通常也应该设为守护线程——程序都退出了,日志线程还跑着干嘛?
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("守护线程工作中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
});
daemonThread.setDaemon(true); // 必须在start之前设置
daemonThread.start();
System.out.println("主线程结束");
// 主线程结束后,守护线程也会自动退出
注意:setDaemon(true)必须在start()之前调用,否则会抛出IllegalThreadStateException。我曾经在项目里因为忘记这个顺序,排查了半天才发现是线程启动顺序的问题。
还有一个坑:守护线程中创建的线程默认也是守护线程。如果你在守护线程里开了子线程做重要任务,主线程一退出,子线程也会被强制终止,数据可能没写完。所以,不要把关键业务逻辑放在守护线程里。
四种创建方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 继承Thread | 简单直接,适合快速测试 | 单继承限制,任务与线程耦合 | 简单演示、小工具 |
| 实现Runnable | 灵活,可继承其他类,任务与线程分离 | 无返回值,不能抛异常 | 大多数日常开发 |
| Callable+Future | 有返回值,可抛异常,支持超时 | 代码稍复杂,需要线程池配合 | 需要获取计算结果的场景 |
| Daemon线程 | 后台自动结束,不阻塞JVM退出 | 不适合关键任务,可能被强制终止 | 日志、监控、后台清理 |
知识体系总览
下面这张图,是我梳理的线程创建与启动的核心脉络。你可以把它当作本章的「地图」——每次写多线程代码前,瞄一眼就知道该选哪种方式。
我的建议:日常开发中,优先考虑Runnable或Callable。继承Thread的方式,除非你确定这个类不需要继承其他类,否则尽量少用。至于Daemon线程,适合做「锦上添花」的后台任务,别把核心逻辑放进去。
好了,线程创建与启动的基础内容就这些。记住:创建线程只是第一步,真正难的是线程间的协作与同步。后面的章节我们会深入探讨锁、条件变量、并发容器等话题。到时候你会发现,今天学的这些创建方式,是后续所有并发编程的基石。
可可精品资料库