本文系统总结了Java多线程编程的核心知识,涵盖线程创建、线程安全、线程通信、线程池等关键技术,帮助开发者掌握高并发编程的核心能力。
一、多线程基础概念
1.1 线程与多线程
- 线程(Thread):程序内部的一条执行流程
- 单线程程序:只有一条执行流程的程序
- 多线程:软硬件上实现的多条执行流程技术(由CPU调度执行)
多线程技术在现代应用中至关重要,尤其是在高并发场景如:
- 消息通信系统
- 电商平台(淘宝、京东)
- 文件上传/下载服务
- 实时数据处理系统
1.2 线程的创建方式
方式一:继承Thread 类
方式二:实现Runnable接口
方式二的匿名内部类写法:
/**
* 目标:掌握多线程创建方式二的匿名内部类写法。
*/
public class ThreadTest2_2 {
public static void main(String[] args) {
// 1、直接创建Runnable接口的匿名内部类形式(任务对象)
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程1输出:" + i);
}
}
};
new Thread(target).start();
// 简化形式1:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程2输出:" + i);
}
}
}).start();
// 简化形式2:
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程3输出:" + i);
}
}).start();
for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
方式三:实现Callable接口
Java提供三种线程创建方式,各有适用场景:
创建方式 | 实现步骤 | 优点 | 缺点 |
---|---|---|---|
继承Thread类 | 1. 定义子类继承Thread 2. 重写run()方法 3. 创建线程对象 4. 调用start() |
编码简单 | 无法继承其他类 |
实现Runnable接口 | 1. 定义任务类实现Runnable 2. 重写run()方法 3. 创建任务对象 4. 将任务交给Thread处理 |
可继承其他类,扩展性强 | 无法直接返回结果 |
实现Callable接口 | 1. 定义任务类实现Callable 2. 重写call()方法 3. 封装为FutureTask对象 4. 交给Thread执行 |
可返回结果,扩展性强 | 编码复杂 |
关键代码示例:
// 方式1:继承Thread
class MyThread extends Thread {
//重写run方法
@Override
public void run() {
System.out.println("Thread running");
}
}
// 方式2:实现Runnable
Runnable task = () -> System.out.println("Runnable running");
new Thread(task).start();
// 方式3:实现Callable
Callable<String> callable = () -> "Result";
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
System.out.println(futureTask.get()); // 获取返回值
1.3 Thread类核心方法
方法 | 作用 | 注意事项 |
---|---|---|
setName(String) |
设置线程名称 | 便于调试和日志跟踪 |
getName() |
获取线程名称 | 默认名称为Thread-序号 |
currentThread() |
获取当前执行线程的引用 | 静态方法 |
sleep(long millis) |
让线程休眠指定毫秒 | 不释放锁 |
join() |
等待该线程执行结束 | 常用于线程顺序执行 |
interrupt() |
中断线程 | 非强制停止,需配合异常处理 |
二、线程安全与同步机制
2.1 线程安全问题
当多个线程同时操作共享资源且存在修改操作时,可能导致数据不一致。
经典案例:银行取款问题
// 共享账户
public class Account {
private double balance = 100000; // 初始余额10万
public void drawMoney(double amount) {
if(balance >= amount) {
balance -= amount; // 问题可能发生在此处
}
}
}
当两个线程同时执行取款操作时:
- 线程A检查余额足够(100000 > 100000)
- 线程B检查余额足够(100000 > 100000)
- 线程A扣款,余额变为0
- 线程B扣款,余额变为-100000
2.2 线程同步解决方案
加锁可以实现线程同步,有三个方法:同步代码块、同步方法、lock锁。
2.2.1 同步代码块
public void drawMoney(double amount) {
synchronized (this) { // 锁对象建议使用共享资源,this正好是共享资源
if(balance >= amount) {
balance -= amount;
}
}
}
2.2.2 同步方法
public synchronized void drawMoney(double amount) {
if(balance >= amount) {
balance -= amount;
}
}
2.2.3 Lock锁
private final Lock lock = new ReentrantLock();
public void drawMoney(double amount) {
lock.lock();
try {
if(balance >= amount) {
balance -= amount;
}
} finally {
lock.unlock(); // 放在finally 确保解锁
}
}
2.3 同步方案对比
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
同步代码块 | 需要精细控制同步范围 | 锁粒度细,性能较好 | 编码稍复杂 |
同步方法 | 整个方法需要同步 | 编码简洁 | 锁粒度粗 |
Lock锁 | 需要高级功能(如超时控制) | 功能强大,可中断 | 需手动释放锁 |
三、线程通信与协调(了解)
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
3.1 生产者-消费者模型
多线程协作的经典模式:
- 生产者线程:生成数据
- 消费者线程:消费数据
- 协调机制:
- 生产者生产完数据后等待,通知消费者
- 消费者消费完数据后等待,通知生产者
3.2 核心方法
方法 | 作用 |
---|---|
wait() |
让当前线程等待并释放锁 |
notify() |
随机唤醒一个等待线程 |
notifyAll() |
唤醒所有等待线程 |
3.3 代码实现:包子铺案例
class Desk {
private List<String> list = new ArrayList<>();
// 生产者方法
public synchronized void put() {
try {
while (!list.isEmpty()) {
wait(); // 有包子就等待
}
list.add("包子");
System.out.println(Thread.currentThread().getName() + "做了一个包子");
notifyAll(); // 唤醒消费者
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费者方法
public synchronized void get() {
try {
while (list.isEmpty()) {
wait(); // 没包子就等待
}
String item = list.remove(0);
System.out.println(Thread.currentThread().getName() + "吃了:" + item);
notifyAll(); // 唤醒生产者
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
四、线程池高级应用
4.1 线程池优势
- 降低资源消耗:复用已创建的线程
- 提高响应速度:任务到达可直接执行
- 提高线程可管理性:统一分配、监控线程
- 防止资源耗尽:通过队列缓冲和拒绝策略
4.2 线程池创建(推荐方式)
ExecutorService pool = new ThreadPoolExecutor(
3, // 核心线程数 (正式员工)
5, // 最大线程数 (正式+临时工)
8, // 临时线程存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(5), // 任务队列 (候客区)
Executors.defaultThreadFactory(), // 线程工厂 (HR部门)
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 (客满处理)
);
4.3 线程池工作原理
4.4 任务提交方式
// 执行Runnable任务
pool.execute(() -> System.out.println("Runnable task"));
// 执行Callable任务并获取结果
Future<String> future = pool.submit(() -> "Callable result");
String result = future.get(); // 阻塞获取结果
4.5 线程池配置建议
任务类型 | 核心线程数公式 | 说明 |
---|---|---|
CPU密集型 | CPU核数 + 1 | 大量计算,少IO等待 |
IO密集型 | CPU核数 × 2 | 大量数据库/网络操作 |
混合型 | (线程等待时间/线程CPU时间 + 1) × CPU核数 | 根据实际比例调整 |
五、并发理论与线程生命周期
5.1 并发 vs 并行
概念 | 定义 | 关键区别 |
---|---|---|
并发(Concurrency) | CPU通过时间片轮转交替执行多个线程 | 逻辑上同时发生 |
并行(Parallelism) | 多个线程在多个CPU核心上真正同时执行 | 物理上同时发生 |
5.2 线程生命周期
Java线程的6种状态及其转换:
状态详解:
- NEW:线程创建但未启动
- RUNNABLE:可运行状态(包含就绪和运行中)
- BLOCKED:等待监视器锁(同步代码块)
- WAITING:无限期等待(wait()方法)
- TIMED_WAITING:限期等待(sleep()/wait(timeout))
- TERMINATED:线程执行完毕
六、总结与最佳实践
6.1 多线程编程核心要点
- 线程创建:优先选择实现Runnable或Callable接口
- 线程安全:同步范围最小化,推荐使用Lock锁
- 线程协调:使用wait/notify实现生产者-消费者模式
- 资源管理:始终使用线程池管理线程资源
- 状态监控:关注线程生命周期状态转换
6.2 最佳实践
- 命名线程:便于调试和问题追踪
new Thread(task, "Payment-Thread").start();
- 使用线程局部变量:避免不必要同步
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
- 优先使用并发集合:如ConcurrentHashMap、CopyOnWriteArrayList
- 避免死锁:按固定顺序获取多个锁
- 使用CompletableFuture:简化异步编程(JDK8+)
掌握多线程技术是Java高级开发的必备能力。通过理解线程原理、熟练使用同步机制和线程池,开发者可以构建出高性能、高并发的应用程序,充分利用现代多核处理器的计算能力。