Java多线程基础:从Thread到Runnable,看这一篇就够了
作者:全栈修炼日记
系列:Java并发编程实战
难度:⭐⭐(适合1-3年经验)
前言
作为一个工作两年的Java开发,我发现很多同学(包括曾经的我)对多线程的理解还停留在"会用Thread和Runnable"的阶段。但当面试官问起线程状态、线程安全、为什么要用start()而不是run()时,就开始支支吾吾了。
今天这篇文章,我会用最简单的语言,带你彻底搞懂Java多线程的基础知识。看完这篇,你至少能回答面试中80%的多线程基础问题。
一、为什么需要多线程?
1.1 单线程的痛点
想象一个场景:你写了一个程序,需要做三件事:
- 从数据库查询数据(耗时2秒)
- 调用第三方API(耗时3秒)
- 处理业务逻辑(耗时1秒)
如果用单线程,总耗时 = 2 + 3 + 1 = 6秒
但如果用多线程,三个任务同时执行,总耗时 = max(2, 3, 1) = 3秒
性能提升一倍!
1.2 多线程的应用场景
- Web服务器:每个请求一个线程,支持并发访问
- 异步任务:发送邮件、生成报表,不阻塞主流程
- 大数据处理:分片并行计算,提升效率
- 实时系统:同时处理多个事件
二、创建线程的两种方式
2.1 方式一:继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程名称:" + Thread.currentThread().getName());
System.out.println("Hello from MyThread!");
}
}
// 使用
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
输出:
线程名称:Thread-0
Hello from MyThread!
2.2 方式二:实现Runnable接口(推荐)
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程名称:" + Thread.currentThread().getName());
System.out.println("Hello from MyRunnable!");
}
}
// 使用
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
2.3 两种方式的对比
| 特性 | 继承Thread | 实现Runnable |
|---|---|---|
| 灵活性 | ❌ Java单继承,无法继承其他类 | ✅ 可以继承其他类 |
| 资源共享 | ❌ 每个线程独立 | ✅ 多个线程可以共享同一个Runnable对象 |
| 解耦性 | ❌ 线程和任务耦合 | ✅ 线程和任务分离 |
| 推荐度 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
结论:优先使用Runnable接口!
三、线程的6种状态(重要!)
Java线程在生命周期中会经历6种状态:
NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
3.1 状态详解
| 状态 | 说明 | 如何进入 |
|---|---|---|
| NEW | 新建状态 | 创建Thread对象,但未调用start() |
| RUNNABLE | 可运行状态 | 调用start()后,等待CPU调度 |
| BLOCKED | 阻塞状态 | 等待获取锁(synchronized) |
| WAITING | 等待状态 | 调用wait()、join() |
| TIMED_WAITING | 超时等待 | 调用sleep(time)、wait(time) |
| TERMINATED | 终止状态 | run()方法执行完毕 |
3.2 状态转换示例
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(2000); // 进入TIMED_WAITING
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("创建后:" + thread.getState()); // NEW
thread.start();
System.out.println("启动后:" + thread.getState()); // RUNNABLE
Thread.sleep(100);
System.out.println("睡眠中:" + thread.getState()); // TIMED_WAITING
thread.join(); // 等待线程结束
System.out.println("结束后:" + thread.getState()); // TERMINATED
}
}
输出:
创建后:NEW
启动后:RUNNABLE
睡眠中:TIMED_WAITING
结束后:TERMINATED
四、start() vs run():面试必问!
4.1 区别
Thread thread = new Thread(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName());
});
// 方式1:调用start()
thread.start();
// 输出:当前线程:Thread-0(新线程)
// 方式2:调用run()
thread.run();
// 输出:当前线程:main(主线程)
关键区别:
start():启动一个新线程,JVM会调用run()方法run():在当前线程中直接执行,相当于普通方法调用
4.2 start()的底层原理
public synchronized void start() {
// 1. 检查线程状态(不能重复启动)
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 2. 加入线程组
group.add(this);
// 3. 调用native方法,创建新线程
start0();
}
private native void start0(); // JVM底层实现
记住:start()只能调用一次,重复调用会抛出IllegalThreadStateException!
五、线程的常用方法
5.1 sleep() - 线程休眠
public class SleepDemo {
public static void main(String[] args) {
System.out.println("开始时间:" + System.currentTimeMillis());
try {
Thread.sleep(2000); // 休眠2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束时间:" + System.currentTimeMillis());
}
}
特点:
- 让当前线程暂停执行
- 不会释放锁(重要!)
- 时间到了自动恢复RUNNABLE状态
5.2 join() - 等待线程结束
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("子线程开始");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程结束");
});
thread.start();
thread.join(); // 主线程等待子线程结束
System.out.println("主线程继续执行");
}
}
输出:
子线程开始
子线程结束
主线程继续执行
5.3 yield() - 线程让步
public class YieldDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("线程1:" + i);
Thread.yield(); // 让出CPU
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("线程2:" + i);
}
});
t1.start();
t2.start();
}
}
特点:
- 让当前线程让出CPU时间片
- 只是建议,不保证一定让出
- 让出后立刻重新竞争CPU
六、线程优先级
6.1 优先级范围
Thread.MIN_PRIORITY = 1 // 最低优先级
Thread.NORM_PRIORITY = 5 // 默认优先级
Thread.MAX_PRIORITY = 10 // 最高优先级
6.2 设置优先级
public class PriorityDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("低优先级线程");
});
Thread t2 = new Thread(() -> {
System.out.println("高优先级线程");
});
t1.setPriority(Thread.MIN_PRIORITY); // 设置为1
t2.setPriority(Thread.MAX_PRIORITY); // 设置为10
t1.start();
t2.start();
}
}
注意:
- 优先级只是建议,不保证一定先执行
- 不同操作系统对优先级的支持不同
- 不要依赖优先级来保证执行顺序!
七、守护线程(Daemon Thread)
7.1 什么是守护线程?
守护线程是为其他线程服务的线程,当所有非守护线程结束时,JVM会自动退出,守护线程也会被强制终止。
典型例子:GC线程
7.2 使用示例
public class DaemonDemo {
public static void main(String[] args) throws InterruptedException {
Thread daemon = new Thread(() -> {
while (true) {
System.out.println("守护线程运行中...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemon.setDaemon(true); // 设置为守护线程
daemon.start();
Thread.sleep(2000);
System.out.println("主线程结束");
// 主线程结束后,守护线程也会自动终止
}
}
输出:
守护线程运行中...
守护线程运行中...
守护线程运行中...
守护线程运行中...
主线程结束
八、实战案例:多线程下载文件
8.1 需求
模拟下载一个100MB的文件,分成4个线程并行下载,每个线程下载25MB。
8.2 代码实现
public class MultiThreadDownload {
private static final int TOTAL_SIZE = 100; // 总大小(MB)
private static final int THREAD_COUNT = 4; // 线程数
public static void main(String[] args) throws InterruptedException {
int partSize = TOTAL_SIZE / THREAD_COUNT; // 每个线程下载的大小
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
final int startPos = i * partSize;
final int endPos = (i + 1) * partSize;
threads[i] = new Thread(() -> {
System.out.println("线程" + threadId + "开始下载:" + startPos + "MB - " + endPos + "MB");
// 模拟下载
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + threadId + "下载完成!");
});
threads[i].start();
}
// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}
System.out.println("文件下载完成!");
}
}
输出:
线程0开始下载:0MB - 25MB
线程1开始下载:25MB - 50MB
线程2开始下载:50MB - 75MB
线程3开始下载:75MB - 100MB
线程0下载完成!
线程1下载完成!
线程2下载完成!
线程3下载完成!
文件下载完成!
九、常见面试题
Q1:创建线程有几种方式?
答: 两种基本方式:
- 继承Thread类
- 实现Runnable接口(推荐)
扩展:还可以用Callable + FutureTask(有返回值)、线程池等。
Q2:start()和run()的区别?
答:
start():启动新线程,JVM调用run()方法run():在当前线程中执行,相当于普通方法调用
Q3:线程有哪几种状态?
答: 6种状态:
- NEW(新建)
- RUNNABLE(可运行)
- BLOCKED(阻塞)
- WAITING(等待)
- TIMED_WAITING(超时等待)
- TERMINATED(终止)
Q4:sleep()和wait()的区别?
答:
sleep():Thread类的方法,不释放锁,时间到自动恢复wait():Object类的方法,释放锁,需要notify()唤醒
Q5:如何停止一个线程?
答:
- ❌ 不要用stop()(已废弃,不安全)
- ✅ 用标志位控制:
public class StopThreadDemo {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// 执行任务
}
}
}
十、总结
本文我们学习了Java多线程的基础知识:
✅ 创建线程:Thread vs Runnable,优先用Runnable
✅ 线程状态:6种状态及转换关系
✅ 核心方法:start()、run()、sleep()、join()、yield()
✅ 守护线程:为其他线程服务,主线程结束时自动终止
✅ 实战案例:多线程下载文件
下一篇预告:《深入理解ThreadPoolExecutor的7个参数》
参考资料
- 《Java并发编程实战》
- Oracle官方文档:docs.oracle.com/javase/tuto…
- 《深入理解Java虚拟机》
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!
有问题欢迎评论区讨论,我会及时回复。作者:从CRUD到架构师
系列:Java并发编程实战
下一篇:《深入理解ThreadPoolExecutor的7个参数》