一文吃透 Java 多线程:从创建到线程池、同步机制与并发集合
本文基于《Java 多线程》课程讲义整理,系统梳理 Java 多线程核心知识:进程与线程的概念、创建线程的多种方式、线程状态流转、线程池、Callable/Future、线程同步(synchronized / Lock)、线程通信(wait/notify)、死锁,以及 juc 包下的线程安全集合类。全文附大量代码示例,适合 Java 初学者复习与面试速览。
目录
- 进程与线程的基本概念
- 创建线程的两种方式
- 线程的状态流转
- 线程常见方法:sleep 与 join
- 线程池:为什么需要 & 怎么用
- Callable 与 Future:带返回值的线程任务
- 线程安全与线程同步
- 线程通信:wait 与 notify
- 死锁
- 线程安全的集合类(JUC)
- 总结
一、进程与线程的基本概念
1.1 什么是进程
进程:操作系统中正在运行的一个应用程序。打开 QQ、迅雷、音乐播放器,每一个都是一个进程。
多进程并发的执行原理可以概括为八个字:宏观并行,微观串行。
- 并发:一个 CPU 采用时间片轮转的方式"同时"执行多个任务。例如秒杀活动,大量请求在极短时间内被单个或少量 CPU 交替处理。
1.2 什么是线程
线程:在一个进程中可以同时执行的多个任务,每一个任务就是一个线程。线程是进程的执行单元,也称为轻量级进程。
主线程:从 main 方法开始执行,以 main 方法的退出作为结束——即 main 函数就是主线程的执行体。
1.3 线程的组成部分
| 组成部分 | 说明 |
|---|---|
| CPU | 操作系统分配时间片 |
| 数据 | 堆空间共享,栈空间独立:每个线程都有自己的栈空间,多个线程可以共享同一个堆空间 |
| 程序代码 | 线程需要执行的指令逻辑 |
堆共享、栈独立这一点,是后续理解"线程安全问题"的根本原因:多个线程共享堆中的同一个对象,就可能出现并发修改冲突。
二、创建线程的两种方式
2.1 方式一:实现 Runnable 接口(推荐)
定义任务类(目标类),把线程要执行的任务写在 run 方法中,再把任务对象交给线程对象去执行:
// 1. 定义任务类,实现 Runnable 接口
class MyTask implements Runnable {
@Override
public void run() {
// 线程的任务定义在 run 方法中
for (int i = 0; i < 100; i++) {
System.out.println("MyTask: " + i);
}
}
}
public class TestRunnable {
public static void main(String[] args) {
// 2. 创建目标对象(任务对象)
MyTask task = new MyTask();
// 3. 创建线程对象,并将目标对象作为参数传递
Thread t1 = new Thread(task);
// 4. 开启线程:调用 start 方法,JVM 会自动运行 run 中的任务代码
t1.start();
}
}
2.2 方式二:继承 Thread 类
定义子类继承 Thread 并覆盖 run 方法,此时该类本身就是线程类:
// 1. 定义线程类,继承 Thread
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("MyThread: " + i);
}
}
}
public class TestThread {
public static void main(String[] args) {
// 2. 创建线程对象
MyThread t2 = new MyThread();
// 3. 开启线程
t2.start();
}
}
2.3 两种方式小结与对比
| 实现 Runnable | 继承 Thread | |
|---|---|---|
| 类的性质 | 任务类(目标类) | 线程类 |
| 创建对象 | 任务对象 + 线程对象 | 线程对象 |
| 开启方式 | new Thread(task).start() | t.start() |
| 扩展性 | 不占用继承名额,推荐 | 占用继承名额 |
注意:一定要调用 start() 而不是直接调用 run()。直接调用 run() 只是普通的方法调用,并不会开启新线程。
三、线程的状态流转
线程在生命周期中会经历以下状态:
New(初始) --start()--> Ready(就绪) --OS选中--> Running(运行) --run()结束--> Terminated(终止)
↑ │
└────时间片到期────────┘
- New(初始状态):线程对象被创建。此时只在堆中开辟内存,与常规对象无异。
- Ready(就绪状态):调用
start()之后进入,等待 OS 选中并分配时间片。 - Running(运行状态):获得时间片后进入;如果时间片到期,则回到就绪状态。
- Terminated(终止状态):主线程
main()或独立线程run()结束,代表线程的任务结束,并释放持有的时间片。
补充状态:
- Timed Waiting(计时等待/限期等待):调用
sleep(ms)进入,到期后回到就绪。 - Waiting(无限期等待):调用
join()或等待数据输入等进入,条件满足后回到就绪。 - Blocked(阻塞状态):线程获取不到锁标记时进入(详见后文 synchronized),获得锁后回到就绪。
注:JDK 5 之后,就绪状态和运行状态统称为 Runnable。
四、线程常见方法:sleep 与 join
4.1 sleep —— 休眠
public static void sleep(long m)
让当前线程休眠 m 毫秒,进入计时等待状态,同时释放 CPU 资源(但不释放锁)。
4.2 join —— 合并线程
public final void join()
允许其他线程加入到当前线程中:调用 join 的线程先执行,等它执行结束后,再执行当前线程自身的任务。
例如在 main 方法中调用 t1.join(),代表主线程让步于 t1 线程:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("t1: " + i);
}
});
t1.start();
t1.join(); // 主线程等待 t1 执行完毕后,才继续执行
System.out.println("main 结束");
}
五、线程池:为什么需要 & 怎么用
5.1 为什么需要线程池
如果每个任务都 new Thread(),任务结束后就销毁线程,会带来严重问题:
- 频繁创建及销毁线程,增加资源开销,虚拟机 GC 回收频率也随之提高,造成程序性能下降。
解决思路:线程完成任务后不立即销毁,而是存放在池中;再有新任务时直接复用已有线程,避免频繁创建和销毁——这就是线程的重复利用。
5.2 线程池的概念
线程池:一个线程容器。预先创建线程对象存入池中,将任务提交给线程池后,由线程池分配线程、运行任务,任务结束后线程被复用。
好处:避免频繁创建和销毁线程,提高程序性能。
5.3 获取线程池(java.util.concurrent 包)
- Executor:线程池的顶级接口。
- ExecutorService:Executor 的子接口,通过
submit(Runnable task)提交任务代码。 - Executors 工厂类:提供大量静态方法获取线程池对象:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
// 固定数量的线程池:参数指定池中线程数量
ExecutorService pool = Executors.newFixedThreadPool(3);
// 提交任务
pool.submit(() -> System.out.println("任务1"));
pool.submit(() -> System.out.println("任务2"));
pool.submit(() -> System.out.println("任务3"));
pool.shutdown(); // 关闭线程池
}
}
常用工厂方法:
| 方法 | 说明 |
|---|---|
newFixedThreadPool(int nThreads) | 固定数量的线程池 |
newCachedThreadPool() | 动态数量的线程池,不够则创建新线程,没有上限 |
六、Callable 与 Future:带返回值的线程任务
6.1 Runnable 的缺点
思考:run 方法有哪些缺点?
- 不能抛出异常(run 方法签名中不含 checked 异常);
- 没有返回值。
6.2 Callable 接口
Callable 位于 java.util.concurrent 包中,JDK 5.0 加入,与 Runnable 类似,实现它即代表一个线程任务。核心方法:
V call() throws Exception // 有泛型返回值、可以抛异常
示例:两个线程分别计算 150 与 51100 的和,最后累加输出:
import java.util.concurrent.*;
public class TestCallable {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
Callable<Integer> task1 = () -> {
int sum = 0;
for (int i = 1; i <= 50; i++) sum += i;
return sum;
};
Callable<Integer> task2 = () -> {
int sum = 0;
for (int i = 51; i <= 100; i++) sum += i;
return sum;
};
Future<Integer> f1 = pool.submit(task1);
Future<Integer> f2 = pool.submit(task2);
System.out.println("总和 = " + (f1.get() + f2.get())); // 5050
pool.shutdown();
}
}
6.3 Future 接口
Future 位于 java.util.concurrent 包中,通过 ExecutorService.submit(Callable c) 的返回值可以存储 call 方法的返回结果:
V get():获取call方法的结果,泛型取决于 Callable 接口的具体数据类型。
七、线程安全与线程同步
7.1 一个经典的并发 Bug
设想一个封装了 String 数组的 MyList 类(长度 5,用 index 记录元素个数),两个线程分别往 list 添加 "C" 和 "D"。多次运行,结果可能出错。
原因分析——对 list 添加元素包括两个过程:
① as[index] = s; // 将元素添加到目标位置
② index++; // 将元素的个数加 1
这两个过程应该作为一个整体一起执行(要执行都执行,不执行都不执行)。但在多线程并发下,两个步骤可能被分开交错执行,导致结果出错。
7.2 三个核心概念
- 临界资源:多线程并发时,多个线程共享的同一个对象。
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可被打乱或缺省。
- 线程同步:多线程并发时,为了保证临界资源的正确性,不能破坏程序中的原子操作。
7.3 同步代码块
对临界资源加锁:
synchronized (临界资源对象) {
// 代码(原子操作)
}
执行原理:线程只有获取到临界资源对象的锁标记,才能执行同步代码块中的内容,且必须将 {} 中所有代码执行完成才释放锁标记;如果没有获取到锁标记(被其他线程占用),该线程进入阻塞状态,直到拿到锁标记,同时获取 CPU 时间片后才执行 {} 中的内容。
7.4 同步方法
public synchronized void add(String s) {
// 代码(原子操作)
}
注意:同步方法等同于同步代码块,即等价于:
synchronized (this) {
// 代码(原子操作)
}
7.5 同步注意事项
- 任意对象都拥有单一的锁标记(监视器);
- 线程同步时,必须保证多个线程共用同一个对象的锁,才能保证共享资源的正确性。
回顾经典面试题——ArrayList 和 Vector 的区别:
| ArrayList | Vector | |
|---|---|---|
| 线程安全 | 不安全 | 安全 |
| 运行效率 | 快 | 慢 |
| 内部方法 | 非同步方法 | 同步方法 |
7.6 Lock 接口
从 JDK 5.0 开始,Java 提供了更强大的线程同步机制:通过定义锁对象,灵活控制锁的开启和释放。Lock 位于 java.util.concurrent.locks 包中:
void lock():获取锁,如锁被占用则等待;void unlock():释放锁;- 常用实现类:
ReentrantLock。
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++; // 原子操作
} finally {
lock.unlock(); // 释放锁(务必放在 finally 中)
}
}
}
八、线程通信:wait 与 notify
当多个线程需要协作交替执行时,需要使用线程通信方法(均属于 Object 类):
wait():等待。让当前线程进入等待状态,释放 CPU 同时释放锁标记,让其他线程可以执行。等待结束后,需要再次获取临界资源对象的锁标记和 CPU 才能继续执行。notify():唤醒处于等待状态的一个线程。notifyAll():唤醒处于等待状态的所有线程。
重要约束:wait() / notify() / notifyAll() 必须使用在调用它的对象的同步代码块(或同步方法)中,否则会抛出 IllegalMonitorStateException。
经典练习:两个线程交替打印 A~Z(t1 打印 A,t2 打印 B,t1 打印 C……依次交替),思路就是用 wait/notify 控制执行权的转移。
注意区分:
sleep释放 CPU 但不释放锁;wait释放 CPU 且释放锁。
九、死锁
死锁:两个或多个线程,相互占用对方需要的资源而都不释放,导致彼此相互等待对方释放资源,产生无限制等待的现象。
结果:出现死锁后不会抛出异常,也没有任何提示,只是所有线程都处于阻塞状态。如果没有外力介入,程序将无法继续。
典型场景:线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1。规避方法包括:保证加锁顺序一致、缩小锁范围、尝试使用 tryLock 设置超时等。
十、线程安全的集合类(JUC)
10.1 CopyOnWriteArrayList
位于 java.util.concurrent 包中,是 List 的实现类:
- 读操作没有加锁,写操作进行加锁,读与写之间不阻塞;
- 写操作时,先拷贝一个副本,在副本基础上进行写操作,最后用新文本替换旧文本(写时复制);
- 牺牲写操作的效率,提升读操作的效率;
- 使用场景:读操作次数远远大于写操作时效率较高;
- 使用方式与 ArrayList 无异。
10.2 ConcurrentHashMap
位于 java.util.concurrent 包中,是 Map 的实现类:
- 采用分段锁,缩小锁定对象的范围,从而减少锁冲突的可能性,提高系统并发能力;
- 使用场景:只有类似
size()这种获取全局信息的方法不频繁时,减小锁粒度的方式才有真正的意义; - 注意:JDK 8.0 及以上版本采用 CAS 算法和 synchronized 保证线程安全。
10.3 Queue 接口(队列)
Queue 是 Collection 的子接口,用于模拟队列数据存储结构:
- FIFO:先进先出(first in first out);
- 常用方法:
boolean add(E e):顺序添加一个元素(到达上限后再添加会抛出异常);E remove():获得第一个元素并移除(队列没有元素时抛异常)。
10.4 BlockingQueue 接口(阻塞队列)
BlockingQueue 是 Queue 的子接口,在队列基础上增加了两个"无限期等待"的方法:
void put(E e):插入元素,如果没有可用空间则等待;E take():获取并移除队头元素,如果没有可用元素则等待。
实现类:
| 实现类 | 结构 | 特点 |
|---|---|---|
ArrayBlockingQueue | 数组 | 有界队列(手工固定上限) |
LinkedBlockingQueue | 链表 | 无界队列 |
10.5 ConcurrentLinkedQueue
Queue 的常用实现类,基于链表实现:
- 无锁并且线程安全,采用比较交换算法,简称 CAS 无锁算法。
十一、总结
| 知识点 | 关键结论 |
|---|---|
| 进程 vs 线程 | 进程是运行中的应用,线程是进程的执行单元;堆共享、栈独立 |
| 创建线程 | 实现 Runnable(推荐)/ 继承 Thread;必须调用 start() |
| 线程状态 | New → Ready → Running → Terminated,补充 Timed Waiting / Waiting / Blocked |
| 线程池 | 复用线程避免频繁创建销毁;Executors 工厂 + ExecutorService.submit |
| Callable/Future | 解决 run 不能抛异常、无返回值的缺点 |
| 线程同步 | synchronized 同步块/方法、Lock/ReentrantLock;必须共用同一把锁 |
| 线程通信 | wait/notify 必须在同步代码块中;wait 释放锁,sleep 不释放 |
| 死锁 | 互占资源不释放,无异常无提示,程序永久阻塞 |
| JUC 集合 | CopyOnWriteArrayList(读多写少)、ConcurrentHashMap(JDK8 CAS+synchronized)、BlockingQueue(put/take 阻塞)、ConcurrentLinkedQueue(CAS 无锁) |
多线程是 Java 面试和实际开发中的绝对高频考点。建议把文中代码亲手跑一遍,尤其是"两个线程同时添加元素"的并发 Bug 和 wait/notify 交替打印 A~Z 这两个练习,理解会深刻很多。
原创整理不易,觉得有帮助请点个赞、收藏一下再走吧~ 欢迎评论区交流讨论!
本文基于 Java Platform SE 课程讲义《多线程》整理,示例代码均可在 JDK 8+ 环境直接运行。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)