目录
一、先搞懂:多线程到底是什么?(核心概念)
二、实战第一步:Java 创建多线程的 2 种核心方式
方式 1:继承 Thread 类(入门首选,代码简单)
核心逻辑
完整可运行代码
运行效果(参考)
易错点提醒
方式 2:实现 Runnable 接口(实战推荐,无继承限制)
完整可运行代码
两种方式对比
三、多线程的 “坑”:线程安全问题(面试 / 实战高频)
第一步:重现问题(模拟售票系统)
运行结果(错误示例)
问题原因
第二步:解决问题 —— 同步锁(synchronized)
方式 1:同步方法(给方法加锁)
方式 2:同步代码块(更灵活)
关键提醒
四、必懂:线程的生命周期(面试常考)
核心方法对应的状态切换
五、进阶:实战必用的 JUC 工具类
1. Callable + Future(带返回值的线程)
2. 线程池(实战首选,复用线程)
常用线程池类型
总结
作为编程学习者,你肯定想系统掌握 Java 多线程的核心知识,既能理解底层逻辑,又能上手实战。这篇内容我会彻底抛弃晦涩术语,用 “概念 + 代码 + 实战避坑” 的模式,从 0 到 1 帮你吃透多线程,看完就能写代码、解问题。
一、先搞懂:多线程到底是什么?(核心概念)
先把核心概念掰碎,用生活例子讲明白,拒绝抽象:
- 进程:正在运行的程序(比如你打开的微信、IDEA),是操作系统分配内存、CPU 等资源的基本单位。类比:你去餐厅吃饭,“餐厅里的你” 就是一个进程,占用了餐桌、餐具这些资源。
- 线程:进程内的最小执行单元(比如微信同时收消息、刷朋友圈、发语音),是 CPU 调度的最小单位。类比:你吃饭时 “夹菜、聊天、玩手机”,每一个动作就是一个线程,共享 “你的时间、餐桌” 这些进程资源。
- 多线程:一个进程里同时跑多个线程,实现 “并发”(看起来同时执行,实际 CPU 快速切换线程)。关键:线程共享进程资源,这是多线程的核心优势,也是 “线程安全问题” 的根源。
二、实战第一步:Java 创建多线程的 2 种核心方式
Java 封装了现成的 API,新手先掌握这 2 种方式,就能快速写出多线程程序,重点记住:启动线程必须用start(),不是直接调run()。
方式 1:继承 Thread 类(入门首选,代码简单)
核心逻辑
继承Thread类 → 重写run()(定义线程要做的事) → 创建线程对象 → 调用start()启动。
完整可运行代码
// 1. 自定义线程类,继承Thread
class MyThread extends Thread {
// 2. 重写run():线程要执行的核心逻辑
@Override
public void run() {
// 模拟线程任务:循环输出5次
for (int i = 0; i < 5; i++) {
// 获取当前线程名称,方便观察执行顺序
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 执行:" + i);
// 模拟耗时操作(让线程切换更明显)
try {
Thread.sleep(100); // 线程休眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类(主线程)
public class ThreadDemo1 {
public static void main(String[] args) {
// 3. 创建2个线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 给线程命名(可选,但建议写,方便调试)
t1.setName("线程A");
t2.setName("线程B");
// 4. 启动线程(核心:start()会创建新线程)
t1.start();
t2.start();
// 主线程也执行逻辑,对比“并发”效果
for (int i = 0; i < 5; i++) {
System.out.println("主线程 执行:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行效果(参考)
主线程 执行:0
线程A 执行:0
线程B 执行:0
主线程 执行:1
线程B 执行:1
线程A 执行:1
…(交替输出,体现多线程并发)
易错点提醒
❌ 不要直接调用run():调用run()只是普通方法调用,不会创建新线程,所有逻辑都会在主线程串行执行,等于白写多线程。
方式 2:实现 Runnable 接口(实战推荐,无继承限制)
Java 是 “单继承” 语言,如果你的类已经继承了其他类(比如BaseService),就没法再继承Thread了 —— 实现Runnable是更灵活的选择,实现 “任务逻辑” 和 “线程对象” 解耦。
完整可运行代码
// 1. 自定义任务类,实现Runnable接口
class MyRunnable implements Runnable {
// 2. 重写run():定义任务逻辑
@Override
public void run() {
for (int i = 0; i < 5; i++) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 执行:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class ThreadDemo2 {
public static void main(String[] args) {
// 3. 创建任务对象(只定义“要做什么”)
MyRunnable task = new MyRunnable();
// 4. 创建线程对象,把任务传入(线程负责“执行任务”)
Thread t1 = new Thread(task, "线程1");
Thread t2 = new Thread(task, "线程2");
// 5. 启动线程
t1.start();
t2.start();
}
}
两种方式对比
| 继承 Thread | 代码简单,直接调用 start () | 单继承限制,任务和线程耦合 | ★★★☆☆ |
| 实现 Runnable | 无继承限制,任务可复用 | 代码略繁琐(需多创建任务对象) | ★★★★★ |
三、多线程的 “坑”:线程安全问题(面试 / 实战高频)
当多个线程同时操作共享资源(比如同一个变量、同一个对象),就会出现 “数据错乱”—— 这是多线程最核心的难点,也是新手最容易踩的坑。
第一步:重现问题(模拟售票系统)
场景:10 张票,3 个窗口同时卖,预期每张票只卖一次,实际会出问题。
// 共享资源:售票机(10张票)
class Ticket implements Runnable {
private int ticketNum = 10; // 所有窗口共用这10张票
@Override
public void run() {
while (true) {
if (ticketNum > 0) {
// 模拟售票耗时(比如用户付钱)
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖出第 " + ticketNum + " 张票");
ticketNum–; // 票数减1
} else {
break;
}
}
}
}
// 测试类
public class ThreadSafeDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread window1 = new Thread(ticket, "窗口1");
Thread window2 = new Thread(ticket, "窗口2");
Thread window3 = new Thread(ticket, "窗口3");
window1.start();
window2.start();
window3.start();
}
}
运行结果(错误示例)
窗口1 卖出第 10 张票
窗口2 卖出第 10 张票 // 重复售票(错误)
窗口3 卖出第 9 张票
窗口1 卖出第 8 张票
窗口3 卖出第 0 张票 // 卖出0张票(错误)
问题原因
3 个线程同时读取ticketNum=10,然后同时执行ticketNum–,导致重复售票;或者某个线程还没来得及减 1,其他线程已经把票数减到 0,出现 “0 张票” 的错误。
第二步:解决问题 —— 同步锁(synchronized)
核心思路:给共享资源 “加锁”,同一时间只有一个线程能拿到锁,操作完释放锁,其他线程再排队。有 2 种常用方式,按需选择:
方式 1:同步方法(给方法加锁)
把操作共享资源的代码封装成synchronized修饰的方法,锁对象是this(当前共享资源对象)。
class Ticket implements Runnable {
private int ticketNum = 10;
// 同步方法:synchronized修饰,自动加锁/释放锁
private synchronized void sellTicket() {
if (ticketNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖出第 " + ticketNum + " 张票");
ticketNum–;
}
}
@Override
public void run() {
while (true) {
if (ticketNum <= 0) break;
sellTicket(); // 调用同步方法
}
}
}
方式 2:同步代码块(更灵活)
只给 “核心代码” 加锁,指定自定义锁对象(必须是多线程共享的同一个)。
class Ticket implements Runnable {
private int ticketNum = 10;
// 自定义锁对象(核心:多个线程共用这一个锁)
private Object lock = new Object();
@Override
public void run() {
while (true) {
// 同步代码块:只锁核心逻辑,减少锁范围
synchronized (lock) {
if (ticketNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖出第 " + ticketNum + " 张票");
ticketNum–;
} else {
break;
}
}
}
}
}
关键提醒
✅ 锁对象必须是多个线程共享的同一个:如果每个线程都创建自己的锁对象,锁会失效,还是会出现线程安全问题。
四、必懂:线程的生命周期(面试常考)
Java 线程从创建到销毁有 6 种状态,理解状态切换,能避免 “线程卡死”“无法启动” 等问题:
核心方法对应的状态切换
- start():新建 → 就绪;
- sleep(long):运行 → 超时等待 → 就绪(时间到自动切换);
- wait():运行 → 等待;notify()/notifyAll():等待 → 就绪;
- join():当前线程阻塞,直到目标线程执行完毕。
五、进阶:实战必用的 JUC 工具类
Java 的java.util.concurrent(JUC)包封装了更高效的多线程工具,新手先掌握这 2 个,就能应对大部分实战场景。
1. Callable + Future(带返回值的线程)
Runnable的run()无返回值,Callable的call()可返回结果,结合Future获取执行结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 实现Callable,指定返回值类型(Integer)
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 模拟计算任务:1-100的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum; // 返回结果
}
}
public class CallableDemo {
public static void main(String[] args) {
// 1. 创建Callable任务
MyCallable task = new MyCallable();
// 2. 封装成FutureTask(用于获取结果)
FutureTask<Integer> futureTask = new FutureTask<>(task);
// 3. 启动线程
new Thread(futureTask, "计算线程").start();
// 4. 获取结果(get()会阻塞,直到线程执行完)
try {
Integer result = futureTask.get();
System.out.println("计算结果:" + result); // 输出5050
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
2. 线程池(实战首选,复用线程)
频繁创建 / 销毁线程会耗资源,线程池复用线程,提升性能,核心用Executors快速创建:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建固定大小线程池(3个线程)
ExecutorService pool = Executors.newFixedThreadPool(3);
// 2. 提交5个任务(线程池复用3个线程执行)
for (int i = 1; i <= 5; i++) {
int taskNum = i;
pool.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务" + taskNum);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 3. 关闭线程池(用完必须关,否则程序不退出)
pool.shutdown();
}
}
常用线程池类型
- newFixedThreadPool(n):固定大小,适合任务量稳定;
- newCachedThreadPool():动态扩容,适合短任务、任务量波动大;
- newSingleThreadExecutor():单线程,任务串行执行;
- newScheduledThreadPool(n):定时 / 周期性任务(比如每隔 1 秒执行)。
总结
掌握这些内容,你已经能搞定 Java 多线程的基础开发和面试核心考点,后续可深入学习Lock锁、线程通信、死锁避免等进阶知识点。记住:多线程的核心是 “实践”,多写代码、多测试,才能真正吃透!文章如有错误欢迎私信我,我会及时解决,如果我的内容对你有帮助和启发,请点赞、评论、收藏。你们的支持就是我更新最大的动力,那么我们下期再见!





