1. 为什么需要手动创建线程?
在Java开发中,我们经常需要处理并发任务。想象一下餐厅里点餐的场景:如果只有一个服务员(单线程),他必须依次处理每位顾客的点单、上菜和结账,效率会非常低下。而多线程就像雇佣多个服务员,可以同时服务不同餐桌的顾客。
Java提供了两种主要的线程创建方式:
- 继承Thread类
- 实现Runnable接口
选择哪种方式取决于你的具体需求。继承Thread类更简单直接,但Java不支持多重继承,所以如果你的类已经继承了其他类,就只能选择实现Runnable接口。这也是为什么在面向对象设计中,我们更推荐使用Runnable接口——它更灵活,也符合"组合优于继承"的设计原则。
重要提示:直接继承Thread类会限制代码的扩展性,在大多数生产环境中,实现Runnable接口是更好的选择。
2. 继承Thread类创建线程
2.1 基础实现步骤
让我们从一个最简单的例子开始,了解如何通过继承Thread类来创建线程:
java复制class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
System.out.println("线程" + Thread.currentThread().getName() + "正在运行");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
thread1.start(); // 启动线程
System.out.println("主线程" + Thread.currentThread().getName() + "继续执行");
}
}
这段代码展示了线程创建的基本流程:
- 创建一个继承Thread的子类
- 重写run()方法,定义线程要执行的任务
- 创建子类实例
- 调用start()方法启动线程
2.2 关键点解析
start() vs run():
- 直接调用run()方法不会创建新线程,只是在当前线程中执行方法
- start()方法会创建新线程,并在新线程中执行run()方法
线程命名:
每个线程都有名称,可以通过getName()获取。我们可以通过构造方法为线程指定名称:
java复制class MyThread extends Thread {
public MyThread(String name) {
super(name); // 调用父类构造方法设置线程名
}
@Override
public void run() {
System.out.println("线程" + this.getName() + "正在运行");
}
}
2.3 实际应用中的注意事项
在实际项目中,直接继承Thread类的方式有一些限制:
- Java不支持多重继承,如果你的类已经继承了其他类,就无法再继承Thread
- 线程创建和任务执行耦合在一起,不符合单一职责原则
- 线程池等高级功能更倾向于使用Runnable接口
我曾经在一个项目中看到有开发者为了使用Thread类而放弃了原本的继承关系,导致代码结构变得混乱。这是一个典型的反面案例,提醒我们要根据实际情况选择合适的线程创建方式。
3. 实现Runnable接口创建线程
3.1 基础实现方式
Runnable接口是更推荐的线程创建方式,它只有一个run()方法:
java复制class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "正在运行");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
这种方式将线程的创建和任务的执行解耦,更加灵活。我们可以将同一个Runnable实例传递给多个Thread对象,实现任务共享。
3.2 使用Lambda表达式简化
从Java 8开始,我们可以使用Lambda表达式进一步简化代码:
java复制public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("线程" + Thread.currentThread().getName() + "正在运行");
});
thread.start();
}
}
这种写法简洁明了,特别适合只需要一次性使用的简单任务。
3.3 参数传递与返回值处理
通过Runnable接口创建的线程可以方便地传递参数:
java复制class MyRunnable implements Runnable {
private final String param;
public MyRunnable(String param) {
this.param = param;
}
@Override
public void run() {
System.out.println("接收到参数: " + param);
}
}
但Runnable接口的run()方法没有返回值。如果需要获取线程执行结果,可以考虑使用Callable接口和Future,这超出了本文讨论范围,但知道这个限制很重要。
4. 线程生命周期与管理
4.1 线程状态转换
理解线程的生命周期对于编写健壮的多线程程序至关重要。Java线程有以下几种状态:
- NEW:线程被创建但尚未启动
- RUNNABLE:线程正在JVM中执行或等待系统资源
- BLOCKED:线程被阻塞,等待监视器锁
- WAITING:线程无限期等待另一个线程执行特定操作
- TIMED_WAITING:线程在指定时间内等待
- TERMINATED:线程已完成执行
我们可以通过Thread.getState()方法获取线程当前状态。
4.2 线程优先级
Java线程有优先级设置(1-10),数值越大优先级越高:
java复制thread.setPriority(Thread.MAX_PRIORITY); // 10
thread.setPriority(Thread.NORM_PRIORITY); // 5
thread.setPriority(Thread.MIN_PRIORITY); // 1
但要注意,线程优先级只是给调度器的建议,并不能保证高优先级线程一定会先执行。不同操作系统对优先级的处理方式可能不同。
4.3 守护线程
Java有两种线程类型:
- 用户线程(默认):JVM会等待所有用户线程结束才退出
- 守护线程:不会阻止JVM退出
设置守护线程:
java复制thread.setDaemon(true); // 必须在start()前调用
守护线程适合执行后台任务,如垃圾回收、心跳检测等。我曾经在一个项目中错误地将关键业务逻辑放在守护线程中,结果当主线程结束时这些任务也被意外终止,导致数据不一致。这是一个需要特别注意的陷阱。
5. 线程安全与同步
5.1 竞态条件问题
当多个线程访问共享资源时,可能会出现竞态条件(Race Condition)。考虑以下计数器示例:
java复制class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
如果多个线程同时调用increment(),最终结果可能小于实际调用次数,因为count++不是原子操作。
5.2 同步解决方案
Java提供了多种同步机制:
- synchronized方法:
java复制public synchronized void increment() {
count++;
}
- synchronized代码块:
java复制public void increment() {
synchronized(this) {
count++;
}
}
- volatile关键字:
java复制private volatile boolean running = true;
volatile保证变量的可见性,但不保证原子性。对于简单的标志位,volatile是轻量级的选择。
5.3 死锁预防
死锁是指两个或多个线程互相持有对方需要的资源,导致所有线程都无法继续执行。死锁的四个必要条件:
- 互斥条件
- 占有并等待
- 非抢占条件
- 循环等待
预防死锁的策略包括:
- 按固定顺序获取锁
- 使用tryLock()设置超时
- 避免嵌套锁
我曾经遇到过一个死锁问题:线程A持有锁1并请求锁2,而线程B持有锁2并请求锁1。通过统一锁的获取顺序(总是先获取锁1再获取锁2),我们解决了这个问题。
6. 线程池基础
虽然直接创建线程很简单,但在生产环境中,我们通常使用线程池来管理线程。线程池的优势包括:
- 降低资源消耗(线程创建/销毁开销)
- 提高响应速度(任务到达时线程已存在)
- 提供更强大的管理功能
Java通过Executors类提供几种常用线程池:
java复制ExecutorService fixedPool = Executors.newFixedThreadPool(5);
ExecutorService cachedPool = Executors.newCachedThreadPool();
ExecutorService singleThread = Executors.newSingleThreadExecutor();
对于需要返回结果的任务,可以使用submit()方法:
java复制Future<?> future = executor.submit(() -> {
// 执行任务
return result;
});
// 获取结果(阻塞)
Object result = future.get();
7. 常见问题与调试技巧
7.1 线程未启动
新手常犯的错误是调用run()而不是start():
java复制thread.run(); // 错误!不会创建新线程
thread.start(); // 正确
7.2 线程安全问题排查
当出现难以复现的并发问题时,可以:
- 检查所有共享变量的访问
- 使用Thread.dumpStack()打印线程堆栈
- 使用jstack工具分析线程状态
7.3 性能优化建议
- 避免过度同步(缩小同步范围)
- 考虑使用并发集合(如ConcurrentHashMap)
- 合理设置线程池大小(CPU密集型 vs IO密集型)
在我的经验中,大多数线程问题都源于对共享状态的不当管理。遵循"尽可能无状态,必要时不可变,最后才考虑同步"的原则,可以显著减少并发问题。
8. 实际项目中的应用案例
让我们看一个真实场景中的线程应用——实现一个简单的文件下载管理器:
java复制class DownloadTask implements Runnable {
private final String url;
private final String savePath;
public DownloadTask(String url, String savePath) {
this.url = url;
this.savePath = savePath;
}
@Override
public void run() {
try {
System.out.println("开始下载: " + url);
// 模拟下载过程
Thread.sleep(2000);
System.out.println("下载完成: " + url);
} catch (InterruptedException e) {
System.out.println("下载被中断: " + url);
}
}
}
public class DownloadManager {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
List<String> urls = Arrays.asList(
"http://example.com/file1",
"http://example.com/file2",
"http://example.com/file3",
"http://example.com/file4",
"http://example.com/file5"
);
for (String url : urls) {
executor.execute(new DownloadTask(url, "downloads/" + url.substring(url.lastIndexOf('/'))));
}
executor.shutdown();
}
}
这个例子展示了:
- 如何定义具体的任务类(DownloadTask)
- 如何使用线程池管理多个下载任务
- 如何优雅地关闭线程池(shutdown())
在实际项目中,我们还需要考虑:
- 下载进度跟踪
- 失败重试机制
- 速度限制
- 磁盘空间检查等
9. 线程创建的替代方案
随着Java发展,现在有更多高级的并发工具可供选择:
- CompletableFuture(Java 8+):
java复制CompletableFuture.runAsync(() -> {
// 异步任务
});
- 并行流(Java 8+):
java复制list.parallelStream().forEach(item -> {
// 并行处理
});
- 虚拟线程(Java 19+,预览特性):
java复制Thread.startVirtualThread(() -> {
// 轻量级线程任务
});
这些新特性提供了更简洁的并发编程方式,但在底层仍然依赖于线程模型。理解基本的线程创建和管理仍然是Java并发编程的基石。
10. 最佳实践总结
根据我多年的Java开发经验,以下是线程创建和使用的最佳实践:
- 优先选择Runnable接口:更灵活,适合大多数场景
- 合理命名线程:便于调试和日志分析
- 处理未捕获异常:设置UncaughtExceptionHandler
- 避免过度创建线程:考虑使用线程池
- 注意资源清理:确保线程能够正确终止
- 编写线程安全代码:谨慎处理共享状态
- 使用现代并发工具:如CompletableFuture等
记住,多线程编程的核心挑战在于管理共享状态和协调线程间的交互。良好的设计往往能减少同步需求,比如使用不可变对象或线程封闭技术。
在真实项目中,我见过太多因为不当使用线程导致的诡异问题。一个特别难忘的案例是:一个看似简单的计数器在高并发下表现异常,最终发现是因为开发者没有意识到long类型的读写在32位JVM上不是原子操作。这个教训告诉我们,即使是最基础的操作,在多线程环境下也需要格外小心。
