Java多线程:从入门到进阶
1. 引入:为什么需要多线程?
1.1 单线程的瓶颈
假设你要下载三个文件,单线程的做法是:一个个下载,总时间 = 文件1 + 文件2 + 文件3。
java
downloadFile1(); // 等待完成
downloadFile2(); // 再等
downloadFile3(); // 最后
// 总耗时很长
如果能让三个下载同时进行,总时间 ≈ 最慢的那个文件 ------ 这就是多线程的价值。
1.2 多线程能做什么
- 提升响应速度:UI不会卡死,后台任务异步处理
- 充分利用CPU:多核CPU真正并行
- 提高资源利用率:一个线程等待IO时,其他线程可继续工作
1.3 需要注意
多线程不是完美的,它带来:
- 线程安全问题:多个线程同时修改共享数据
- 性能开销:创建、切换线程有成本
- 死锁风险:互相等待对方释放资源
2. 线程的创建方式
Java中有三种常见方式创建线程。
2.1 继承Thread类
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行");
}
}
// 使用
MyThread t1 = new MyThread();
t1.start(); // 启动线程(不是调用run)
特点:
- 简单直接
- 单继承局限(不能再继承其他类)
2.2 实现Runnable接口
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行");
}
}
// 使用
Thread t1 = new Thread(new MyRunnable());
t1.start();
Lambda简化(推荐):
java
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
});
t1.start();
特点:
- 避免单继承局限
- 更适合资源共享(多个线程执行同一个Runnable实例)
2.3 实现Callable接口 + FutureTask
有返回值的线程:
java
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return 100;
}
}
// 使用
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
Thread t1 = new Thread(task);
t1.start();
Integer result = task.get(); // 获取返回值(会阻塞直到完成)
特点:
- 可以有返回值
- 可以抛异常
2.4 三种方式对比
| 方式 | 是否有返回值 | 可否抛异常 | 是否单继承 | 适用场景 |
|---|---|---|---|---|
| 继承Thread | 否 | 否 | 是 | 简单任务 |
| Runnable | 否 | 否 | 否 | 推荐,资源共享 |
| Callable | 是 | 是 | 否 | 需要返回结果 |
3. 线程的常用方法
| 方法 | 作用 |
|---|---|
start() |
启动线程,JVM调用run() |
run() |
线程执行体,不要直接调用 |
sleep(long millis) |
线程睡眠(让出CPU,不释放锁) |
join() |
等待该线程终止 |
yield() |
礼让,让出CPU,重新参与竞争 |
setPriority(int) |
设置优先级(1~10,默认5) |
interrupt() |
中断线程(设置中断标志) |
currentThread() |
获取当前执行的线程 |
java
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("t1: " + i);
Thread.sleep(500);
}
});
Thread t2 = new Thread(() -> {
try {
t1.join(); // t2等待t1执行完
} catch (InterruptedException e) {}
System.out.println("t2执行");
});
t1.start();
t2.start();
4. 线程的生命周期(状态)
线程有6种状态:
NEW → RUNNABLE → TERMINATED
↙ ↘
BLOCKED WAITING / TIMED_WAITING
| 状态 | 说明 |
|---|---|
| NEW | 创建但未启动(还没调用start) |
| RUNNABLE | 可运行状态(正在JVM中执行,可能等待CPU) |
| BLOCKED | 阻塞,等待获取锁(synchronized) |
| WAITING | 无限等待(wait()、join()无超时) |
| TIMED_WAITING | 限时等待(sleep()、wait(timeout)) |
| TERMINATED | 结束(run执行完) |
java
Thread t = new Thread(() -> {});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE(可能瞬间变化)
5. 线程安全问题
5.1 问题演示
多个线程同时修改共享变量:
java
class Counter {
private int count = 0;
public void increment() { count++; }
public int getCount() { return count; }
}
// 两个线程各加1000次
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(counter.getCount()); // 期望2000,实际可能小于2000
原因 :count++不是原子操作(读-改-写),线程交叉执行导致数据丢失。
5.2 解决方案:synchronized
同步代码块
java
public void increment() {
synchronized (this) { // 锁当前对象
count++;
}
}
同步方法
java
public synchronized void increment() { // 等效于synchronized(this)
count++;
}
静态同步方法(类锁)
java
public static synchronized void staticMethod() { // 锁Class对象
// ...
}
5.3 锁的选择
| 方式 | 锁对象 | 影响范围 |
|---|---|---|
| 非静态同步方法 | this |
同一个对象的多个方法互斥 |
| 静态同步方法 | 类名.class |
所有对象的该方法互斥 |
| 同步代码块 | 自定义 | 更灵活,可减小锁粒度 |
6. 线程通信:wait/notify
生产者-消费者模式经典示例:
java
class Product {
private int count = 0;
private final int MAX = 10;
public synchronized void produce() throws InterruptedException {
while (count >= MAX) {
wait(); // 满了,等待消费
}
count++;
System.out.println("生产,库存:" + count);
notifyAll(); // 唤醒等待的消费者
}
public synchronized void consume() throws InterruptedException {
while (count <= 0) {
wait(); // 空了,等待生产
}
count--;
System.out.println("消费,库存:" + count);
notifyAll();
}
}
6.1 wait/notify的规则
| 规则 | 说明 |
|---|---|
| 必须在synchronized块内 | 否则抛IllegalMonitorStateException |
| wait释放锁 | 让出锁,进入WAITING |
| notify不释放锁 | 唤醒另一个线程,但需要当前线程退出同步块后才实际竞争 |
| 用while循环判断条件 | 防止虚假唤醒 |
6.2 wait和sleep的区别
| 维度 | wait | sleep |
|---|---|---|
| 所属 | Object的方法 | Thread的静态方法 |
| 释放锁 | 是 | 否 |
| 需要synchronized | 是 | 否 |
| 唤醒条件 | notify/notifyAll | 时间到或interrupt |
7. 线程安全的集合
| 集合 | 线程安全方式 | 适用场景 |
|---|---|---|
Vector / Hashtable |
内部方法synchronized | 遗留类,不推荐 |
Collections.synchronizedXxx |
包装成同步 | 简单的同步需求 |
CopyOnWriteArrayList |
写时复制 | 读多写少 |
ConcurrentHashMap |
分段锁/CAS | 高并发Map |
BlockingQueue |
阻塞队列 | 生产者-消费者 |
java
// 推荐:ConcurrentHashMap
Map<String, String> map = new ConcurrentHashMap<>();
// 线程安全的List
List<String> list = new CopyOnWriteArrayList<>();
8. 并发问题的底层根源(了解)
| 根源 | 说明 |
|---|---|
| 可见性 | 一个线程修改共享变量,其他线程看不到(用volatile解决) |
| 原子性 | 操作不是一步完成(用synchronized或Lock) |
| 有序性 | 指令重排序(volatile禁止重排序) |
volatile关键字
- 保证可见性:修改立刻写回主存
- 禁止指令重排序
- 不保证原子性 (如
count++仍不安全)
java
volatile boolean flag = true; // 适合用作开关
9. 线程池
9.1 为什么需要线程池?
每次创建和销毁线程都有开销:
new Thread()创建线程 → 内存分配、系统调用- 线程用完销毁 → 垃圾回收
如果有大量短任务,反复创建销毁线程会严重影响性能。
线程池的优势:
- 复用线程,减少创建销毁开销
- 控制线程数量,防止资源耗尽
- 统一管理任务队列
9.2 线程池的核心:ThreadPoolExecutor
java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
执行流程:
提交任务 → 核心线程未满 → 创建核心线程执行
→ 核心线程已满 → 任务放入队列
→ 队列已满 → 创建非核心线程执行
→ 达到最大线程 → 执行拒绝策略
9.3 五种常见的线程池
| 方法 | 说明 | 特点 |
|---|---|---|
newFixedThreadPool(n) |
固定线程数 | 核心=最大=n,无超时 |
newCachedThreadPool() |
缓存线程池 | 核心=0,最大=无限,空闲60秒回收 |
newSingleThreadExecutor() |
单线程池 | 核心=最大=1,保证顺序执行 |
newScheduledThreadPool(n) |
定时任务池 | 支持延迟和执行周期任务 |
newWorkStealingPool() |
工作窃取池 | 基于ForkJoinPool(Java 8+) |
java
// 固定大小线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
// 提交任务
pool.execute(() -> {
System.out.println("无返回值任务");
});
Future<String> future = pool.submit(() -> {
return "有返回值任务";
});
// 关闭线程池
pool.shutdown(); // 不再接受新任务,等待已有任务完成
pool.shutdownNow(); // 尝试停止所有执行中任务
9.4 任务队列的选择
| 队列类型 | 特点 | 适用场景 |
|---|---|---|
ArrayBlockingQueue |
有界,数组结构 | 任务数量可控 |
LinkedBlockingQueue |
有界/无界,链表结构 | 默认无界,小心OOM |
SynchronousQueue |
不存储任务,需立即处理 | cached线程池使用 |
PriorityBlockingQueue |
有优先级 | 任务需排序 |
9.5 使用线程池的注意事项
| 易错点 | 说明 |
|---|---|
不推荐Executors.newFixedThreadPool |
阻塞队列无界,可能OOM |
不推荐Executors.newCachedThreadPool |
最大线程无限,可能创建过多线程 |
| 忘记关闭线程池 | 程序可能不会退出 |
| shutdown和shutdownNow混淆 | shutdown不再收任务但执行已有;shutdownNow尝试中断 |
| 异常被吞掉 | submit返回的Future可以捕获异常 |
阿里巴巴规范 :不要用Executors创建线程池,要自定义ThreadPoolExecutor。
9.6 完整示例
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, 5, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
r -> {
Thread t = new Thread(r);
t.setName("MyPool-" + System.currentTimeMillis());
return t;
},
(r, executor) -> {
System.out.println("任务被拒绝:" + r.toString());
}
);
// 提交任务
for (int i = 0; i < 200; i++) {
final int taskId = i;
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行任务" + taskId);
});
}
pool.shutdown();
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow();
}
} catch (InterruptedException e) {
pool.shutdownNow();
}
10. 死锁
10.1 死锁示例
两个线程互相持有对方需要的锁:
java
Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(() -> {
synchronized (lockA) {
Thread.sleep(100);
synchronized (lockB) { }
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
Thread.sleep(100);
synchronized (lockA) { }
}
});
10.2 避免方法
- 按固定顺序获取锁
- 使用
tryLock超时放弃 - 减少锁的嵌套
11. 易错点总结
| 易错点 | 错误原因 | 正确做法 |
|---|---|---|
| 直接调用run() | 以为会启动线程 | 调用start() |
| 静态方法synchronized混用 | 锁对象不同 | 明确锁是类还是实例 |
| wait/notify不用while判断 | 虚假唤醒 | 用while循环 |
| 线程安全问题只想到synchronized | 简单粗暴 | 考虑volatile、原子类、安全集合 |
| 死锁排查困难 | 互相等待 | 用jstack分析 |
| 共享变量不加同步 | 以为不会并发 | 所有共享变量都要同步 |
| Executors创建线程池 | 无界队列OOM | 自定义ThreadPoolExecutor |
| 忘记关闭线程池 | 程序不退出 | finally中shutdown |
12. 总结对比表
| 概念 | 核心要点 |
|---|---|
| 创建线程 | 三种方式:Thread、Runnable、Callable |
| 生命周期 | NEW → RUNNABLE → TERMINATED(中间有阻塞/等待) |
| 线程安全 | synchronized、Lock、原子类、安全集合 |
| 线程通信 | wait/notify、BlockingQueue |
| 并发关键字 | volatile(可见性、禁止重排)、synchronized(原子性) |
| 线程池 | ThreadPoolExecutor核心参数、任务队列 |
| 死锁 | 循环等待,按顺序加锁或tryLock |