Java 并发入门:从0到1理解线程(实战+避坑指南)
作者:刚毕业的Java后端工程师
前言:并发编程是Java后端开发的核心高频考点与必备技能。本文聚焦实战,不讲虚的理论,从线程创建到常见陷阱,旨在让初学者也能跟着代码掌握并发基础。
并发编程是日常开发(如接口高并发、定时任务、批量处理)和面试中的高频内容,也是新手容易踩坑的地方。
与Spring Boot这类框架不同,并发编程更偏抽象。但只要抓住「线程=独立执行的代码块」这一核心概念,结合实战案例,就能快速理解。本文内容分为三部分:线程基础概念→3种创建线程方式→新手必避的5个坑,全程附带可运行代码。
💡 核心:并发编程的本质是「多线程同时执行」,目的是提升程序效率(例如让接口能同时处理多个请求)。对于新手而言,先掌握「如何创建线程」和「如何控制线程」,就能应对80%的日常开发场景。
一、核心概念解析(新手理解即可)
首先花几分钟理清基础概念,以避免后续写代码时感到困惑:
| 概念 | 通俗解释 | 日常开发场景 |
|---|---|---|
| 进程 | 运行中的程序(如IDEA、浏览器),是系统资源分配的最小单位 | 启动一个Spring Boot项目,就是一个Java进程 |
| 线程 | 进程内的执行单元(可视为干活的"工人"),是CPU调度的最小单位 | 一个Spring Boot进程中,处理不同接口请求的是不同的线程 |
| 并发 | 多个线程"交替执行"(看起来像是同时),例如单个CPU处理多个线程 | 单台服务器处理多个用户的并发接口请求 |
| 并行 | 多个线程"真正同时执行",这需要多核CPU的支持 | 在多核服务器上同时处理多个大数据计算任务 |
📌 新手类比:可以将「进程」比作一家工厂,「线程」则是工厂里的工人。工厂(进程)提供资源(场地、工具),工人(线程)负责具体工作,多个工人(多线程)一起工作就构成了并发或并行。
二、3种创建线程的方式(附实战代码)
Java创建线程主要有3种核心方式。新手应优先掌握前两种,第三种则是日常开发的主流方式。以下将逐一讲解并附上可运行的完整代码。
方式1:继承 Thread 类(基础入门)
核心逻辑:创建一个类继承 Thread类,重写其 run()方法(定义线程要执行的代码),然后调用 start()方法来启动线程。
注意:启动线程必须调用 start(),而不是直接调用 run()。
完整代码
csharp
/**
* 方式1:通过继承Thread类创建线程
* 关键点:start()用于启动新线程,run()只是定义了线程执行体。
*/
public class ThreadDemo1 extends Thread {
// 重写run()方法,定义线程要执行的逻辑
@Override
public void run() {
// 模拟线程任务:打印5次当前线程名称
for (int i = 0; i < 5; i++) {
System.out.println("线程1执行中 → " + Thread.currentThread().getName() + ",次数:" + i);
// 模拟任务耗时,让执行效果更明显
try {
Thread.sleep(500); // 休眠500毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 主方法(主线程),用于测试线程运行
public static void main(String[] args) {
// 1. 创建线程实例
ThreadDemo1 thread1 = new ThreadDemo1();
// 2. 为线程命名(便于调试,建议新手加上)
thread1.setName("新手线程-1");
// 3. 启动线程(核心步骤!)
thread1.start();
// 主线程也执行打印,以对比多线程的并发效果
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行中 → " + Thread.currentThread().getName() + ",次数:" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行效果(关键:观察并发特征)
css
主线程执行中 → main,次数:0
线程1执行中 → 新手线程-1,次数:0
主线程执行中 → main,次数:1
线程1执行中 → 新手线程-1,次数:1
主线程执行中 → main,次数:2
线程1执行中 → 新手线程-1,次数:2
...(两个线程的输出会交替出现,体现了并发执行)
新手要点
- ❌ 错误做法 :直接调用
thread1.run()。这仅仅是普通的方法调用,会在当前(主)线程中执行run()方法,不会创建新线程。 - ✅ 正确做法 :调用
thread1.start()。JVM 会为此创建一个新的线程,并自动在新线程中执行run()方法里的逻辑。
方式2:实现 Runnable 接口(推荐,避免单继承限制)
Java是单继承语言,一个类继承了 Thread后便无法再继承其他类。因此,更推荐实现 Runnable接口的方式,它更灵活,并且符合"组合优于继承"的原则。
完整代码
csharp
/**
* 方式2:通过实现Runnable接口创建线程
* 优势:避免单继承限制,任务逻辑与线程执行分离,可复用性更强。
*/
public class ThreadDemo2 implements Runnable {
// 重写run()方法,定义任务逻辑
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程2执行中 → " + Thread.currentThread().getName() + ",次数:" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 1. 创建任务实例(即线程要执行的逻辑)
Runnable task = new ThreadDemo2();
// 2. 创建Thread对象,并将任务绑定给它
Thread thread2 = new Thread(task);
thread2.setName("新手线程-2");
// 3. 启动线程
thread2.start();
// 主线程逻辑(用于对比)
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行中 → " + Thread.currentThread().getName() + ",次数:" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
核心区别
- 方式1(继承Thread) :线程本身(
Thread)和要执行的任务(run()方法)是绑定在一起的。 - 方式2(实现Runnable) :将线程(
Thread)和任务(Runnable)分离开来。Runnable对象只定义任务逻辑,由Thread对象负责执行。这种方式更加灵活,同一个Runnable任务可以被多个线程执行。
方式3:实现 Callable 接口(带返回值,日常开发主流)
前两种方式创建的线程,其 run()方法没有返回值,也无法抛出受检异常(Checked Exception)。Callable接口解决了这两个问题,它通常与 FutureTask结合使用,以获取线程的执行结果。
完整代码
java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* 方式3:通过实现Callable接口创建线程(带返回值)
* 适用场景:需要获取线程执行结果的场景,例如计算任务、查询数据库等。
*/
public class ThreadDemo3 implements Callable<Integer> {
// call()方法:定义任务,可返回结果,也可抛出异常
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
System.out.println("线程3计算中 → " + i + ",当前和:" + sum);
Thread.sleep(300);
}
return sum; // 返回计算结果
}
public static void main(String[] args) {
// 1. 创建Callable任务
Callable<Integer> task = new ThreadDemo3();
// 2. 用FutureTask包装Callable,用于接收返回值
FutureTask<Integer> futureTask = new FutureTask<>(task);
// 3. 创建线程并启动
Thread thread3 = new Thread(futureTask);
thread3.setName("计算线程-3");
thread3.start();
// 主线程获取子线程的执行结果
try {
// futureTask.get() 会阻塞,直到线程执行完毕并返回结果
Integer result = futureTask.get();
System.out.println("线程3执行完成 → 1-10的和为:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行效果
erlang
线程3计算中 → 1,当前和:1
线程3计算中 → 2,当前和:3
...
线程3计算中 → 10,当前和:55
线程3执行完成 → 1-10的和为:55
核心优势
- 有返回值 :
call()方法可以返回任意类型的执行结果。 - 可抛异常 :
call()方法允许抛出受检异常,调用方可以通过Future.get()捕获处理。 - 可取消任务 :
FutureTask支持通过cancel()方法取消尚未完成的任务。
三、新手必避的5个常见陷阱
结合常见的错误实践,总结了以下5个新手高频陷阱,避开它们可以大幅减少调试时间。
陷阱1:误用 run() 方法代替 start()
-
❌ 错误代码:
iniThreadDemo1 thread1 = new ThreadDemo1(); thread1.run(); // 这只是普通方法调用,没有创建新线程! -
✅ 原因与正解 :
run()方法仅仅是定义了线程要执行的任务体,直接调用它并不会启动新线程。启动新线程必须调用start()方法。
陷阱2:重复启动同一个线程
-
❌ 错误代码:
scssthread1.start(); thread1.start(); // 抛出IllegalThreadStateException -
✅ 原因与正解 :一个
Thread对象的生命周期状态是不可逆的。一旦调用start()进入就绪(RUNNABLE)或运行状态后,就不能再次启动。如果需要再次执行任务,需要创建一个新的Thread实例。
陷阱3:忽略线程安全问题(最常见且隐蔽)
-
❌ 问题示例:多个线程同时读写同一个共享变量。
csharpprivate static int count = 0; // 共享变量 public void run() { for (int i = 0; i < 1000; i++) { count++; // 非原子操作,可能导致结果不准确 } } -
✅ 解决方案:使用同步机制或线程安全的工具类。
- 加锁 :使用
synchronized关键字。 - 使用原子类 :例如
AtomicInteger。
csharpprivate static AtomicInteger count = new AtomicInteger(0); public void run() { for (int i = 0; i < 1000; i++) { count.incrementAndGet(); // 原子性的增加操作 } } - 加锁 :使用
陷阱4:误解 Thread.sleep(0) 的作用
- ❌ 误区 :认为
sleep(0)表示不睡眠。 - ✅ 解释与正解 :
Thread.sleep(0)的作用是让当前线程释放剩余的时间片,主动让出CPU,给其他线程一个执行机会。这通常用于实现"线程谦让",在复杂的并发控制中可能用到。对于新手,在普通业务代码中无需刻意使用,以免打乱程序正常的执行流。
陷阱5:主线程提前结束导致子线程被强制终止
- ❌ 问题场景:在类似Spring Boot的应用中,如果在主线程(或Web容器的主线程)中创建并启动了子线程来处理后台任务,当主线程执行完毕或容器关闭时,这些子线程可能会被强制中断,导致任务未完成。
- ✅ 解决方案 :使用线程池(
ExecutorService)来统一管理线程的生命周期,而不是手动创建和启动Thread对象。线程池能更好地处理线程的创建、复用和优雅关闭。
四、核心总结
本文要点回顾
-
理解核心概念:区分进程、线程、并发与并行,掌握"工厂-工人"的类比模型。
-
掌握三种创建方式:
- 继承
Thread类:最基础,但受限于单继承。 - 实现
Runnable接口:更灵活,推荐使用。 - 实现
Callable接口:可返回结果和抛出异常,适用于需要获取执行结果的场景。
- 继承
-
避开常见陷阱 :重点注意正确启动线程(
start())、避免重复启动、高度重视共享资源的线程安全问题,并理解使用线程池管理线程的必要性。
后续学习方向
掌握了线程的基本创建与管理后,下一步应系统学习:
- Java线程池(
ExecutorService) :这是企业级开发中管理线程的绝对标准,理解其核心参数(核心线程数、最大线程数、工作队列等)及四种常用线程池的适用场景。 - 线程同步机制 :深入学习
synchronized关键字、Lock接口及其实现(如ReentrantLock),以解决复杂的线程安全问题。 - 线程间通信 :了解
wait()、notify()/notifyAll()机制以及更高级的Condition对象。 - 并发工具类 :熟练掌握
JUC (java.util.concurrent)包下的CountDownLatch、CyclicBarrier、Semaphore、ConcurrentHashMap等高级工具。
并发编程是Java后端工程师能力进阶的关键分水岭。从理解线程基础开始,逐步构建完整的并发知识体系,是应对高性能、高并发业务场景的坚实基础。