Java多线程详解
1. 多线程概述
什么是线程
线程是程序中的执行线程。Java虚拟机允许应用程序并发地运行多个执行线程。
核心要点:
- 线程是程序执行的最小单位
- 多线程可以提高程序的执行效率
- 线程之间可以并发执行,共享进程资源
- 正确创建和使用多个线程至关重要
2. 创建线程的两种方法
Java中创建线程有两种主要方法:继承Thread类和实现Runnable接口。
2.1 方法一:继承Thread类
实现步骤:
- 继承Thread类
- 重写Thread类的
run()方法 - 调用线程的
start()方法启动线程
特点: start()方法开启run()并发执行该线程。
完整示例
java
// 第一个线程类继承Thread类
public class Thread1 extends Thread {
public void run() {
// 执行60次循环输出
for (int i = 0; i < 60; i++) {
System.out.println("ThreadOne::" + i);
}
}
}
// 第二个线程类继承Thread类
public class Thread2 extends Thread {
public void run() {
// 执行60次循环输出
for (int i = 0; i < 60; i++) {
System.out.println("ThreadTwo::: " + i);
}
}
}
// 主方法
public class ThreadMain {
// 同步执行两个线程
public static void main(String[] args) {
// 创建两个线程对象
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
// 使用线程的start方法同步执行两个线程对象
t1.start();
t2.start();
}
}
输出结果: 两个线程对象随机交替执行输出语句。
2.2 方法二:实现Runnable接口(推荐)
实现步骤:
- 定义类实现Runnable接口
- 覆盖Runnable接口中的
run()方法,将线程需要运行的代码写入 - 通过Thread类建立带Runnable接口的子类对象参数的线程对象
- 调用Thread类的
start()方法开启线程并调用Runnable接口子类的run()方法
完整示例:模拟卖票窗口
java
// 实现Runnable接口的线程类,模拟卖票窗口
public class Ticket implements Runnable {
// 定义票数
private int tick = 100;
// 重写Runnable接口run()方法
public void run() {
while (true) {
if (tick > 0) {
// 这里多个线程内共享一个tick对象
System.out.println(Thread.currentThread().getName() +
":sale:" + tick--);
} else {
// 卖完打烊
break;
}
}
}
}
public class TicketMain {
public static void main(String[] args) {
// 建立实现Runnable接口的线程对象
Ticket t = new Ticket();
// 将实现Runnable接口的子类当作Thread类的参数,
// 用Thread类的start()方法来开启多个线程
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
2.3 两种方法的对比
| 比较项目 | 继承Thread类 | 实现Runnable接口 |
|---|---|---|
| 继承限制 | 无法再继承其他类 | 避免单继承局限性 ✓ |
| 资源共享 | 每个线程独立的资源 | 多个线程共享同一个对象 ✓ |
| 获取当前线程 | 直接使用this |
使用Thread.currentThread() |
| 代码复用性 | 较差 | 较好 ✓ |
| 推荐程度 | 不推荐 | 推荐使用 ✓ |
结论: 实现Runnable接口的方式更加灵活,是创建线程的推荐方法。
3. 线程安全性
3.1 线程安全问题
问题描述: 当多个线程同时操作同一个共享数据时,如果一个线程对多条语句只执行了一部分,还没全部执行完,另一个线程就参与进来执行,可能会导致共享数据的错误。
解决方案: 对多条操作共享数据的语句,只能让一个线程执行完,才可以让其他线程参与运行。
3.2 线程同步机制
Java提供了synchronized关键字实现线程同步(JDK 1.5之前的主要方式)。
同步代码块
java
// 定义锁对象
Object obj = new Object();
synchronized(obj) {
// 需要同步的代码块
}
同步方法
概念: 同步方法就是给某个方法加锁,从而避免不同对象调用同一个方法出现数据错误的问题。
完整银行存款示例:
java
// 描述银行
public class Bank {
private int sum;
// 银行具有存钱方法,因为执行语句较多所以需要方法的同步
// 方法的同步锁是this
public synchronized void add(int n) {
sum = sum + n;
try {
Thread.sleep(10);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("sum=" + sum);
}
}
// 描述操作银行的线程类,run方法是操作银行存入动作
public class Cus implements Runnable {
private Bank b = new Bank();
// 存三次,每次存入100
public void run() {
for (int x = 0; x < 3; x++) {
b.add(100);
}
}
}
// 主方法
public class BankMain {
public static void main(String[] args) {
// 创建线程,实现多线程操作
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
3.3 同步锁的说明
| 同步类型 | 锁对象 | 说明 |
|---|---|---|
| 同步代码块 | 自定义对象 | 可以是任意对象 |
| 同步方法 | this |
当前实例对象 |
| 静态同步方法 | 类名.class |
类对象 |
⚠️ 重要提示: 多线程死锁问题需要特别注意,避免相互等待造成程序停止。
4. 线程的生命周期
4.1 线程状态详解
markdown
新建 → 就绪 ⇄ 运行 → 消亡
↕ ↓
阻塞 ←───
1. 新建状态(New)
- 触发条件: 当new一个Thread或其子类建立线程后
- 状态描述: 线程对象处于新生状态,还未启动
2. 就绪状态(Runnable)
- 触发条件: 调用
start()方法后进入 - 状态描述: 线程具备了运行条件,但未必立刻执行
- 💡 提示: 可使用
Thread.sleep()方法使主线程休眠,让其他线程有机会执行
3. 运行状态(Running)
- 触发条件: 线程抢到CPU执行权
- 可能的操作:
- 睡眠操作: 调用
sleep()方法 → 进入阻塞状态 - 等待操作: 调用
wait()方法 → 进入阻塞状态(需要notify()或notifyAll()唤醒) - IO阻塞: 调用阻塞式IO方法 → 进入阻塞状态
- 退让暂停: 调用
yield()方法 → 重新进入就绪状态(不进入阻塞状态)
- 睡眠操作: 调用
4. 阻塞状态(Blocked)
- 进入条件: 由运行状态转变而来
- 阻塞类型及恢复方式:
| 阻塞类型 | 进入方式 | 恢复方式 |
|---|---|---|
| 睡眠阻塞 | sleep() |
指定时间后自动恢复到就绪状态 |
| 等待阻塞 | wait() |
notify()或notifyAll()唤醒 |
| IO阻塞 | 阻塞式IO方法 | 外设完成IO操作后自动恢复 |
5. 消亡状态(Dead)
- 触发条件: 从
run()方法正常退出 - 状态描述: 线程执行结束,进入消亡状态
4.2 状态转换图
scss
[新建] --start()--> [就绪] <--yield()-- [运行]
↑ ↓
| sleep()/wait()/IO阻塞
| ↓
恢复 <------------- [阻塞]
↑
|
[消亡] <--run()结束-- [运行]
5. 等待唤醒操作(线程间通信)
5.1 等待唤醒机制概述
使用场景: 多个线程同步操作,但操作的动作不同。比如:对同一个资源的存储和读取操作。
目标: 让CPU运行一次存储线程,就运行一次读取线程。
核心方法: wait()、notify()、notifyAll()
5.2 等待唤醒方法说明
| 方法名 | 功能描述 | 使用方式 |
|---|---|---|
wait() |
让当前线程进入等待状态 | this.wait() |
notify() |
唤醒等待池中的一个线程 | this.notify() |
notifyAll() |
唤醒等待池中的所有线程 | this.notifyAll() |
⚠️ 注意事项:
- 等待和唤醒方法必须标识出所属的锁
- 必须在同步中使用,因为要对持有监视器(锁)的线程操作
- 操作的线程必须使用同一个锁
5.3 生产者-消费者模式实现
基础版本(适用于单生产者-单消费者)
java
// 封装商品和生产、消费商品的方法
public class Resource {
private String name;
private int count = 1; // 产品编号
private boolean flag = false; // 标记:true可消费,false可生产
// 生产操作,如果消费过一次就得存入一次商品
public synchronized void set(String name) {
if (flag) {
try {
wait();
} catch(InterruptedException e) {}
}
this.name = name + "..." + count++;
System.out.println(Thread.currentThread().getName() +
"..生产。。" + this.name);
// 改变标记,好让消费线程成功运行,true代表可消费
flag = true;
// 仅考虑两个线程操作,所以唤醒对方
this.notify();
}
// 消费操作,如果生产过商品了,就消费一次商品
public synchronized void out() {
if (!flag) {
try {
wait();
} catch(InterruptedException e) {}
}
System.out.println(Thread.currentThread().getName() +
"。消费。" + this.name);
// 改变标记,生产线程false代表可生产
flag = false;
// 仅考虑两个线程操作,所以唤醒对方
this.notify();
}
}
// 生产者线程
public class Producer implements Runnable {
private Resource res;
Producer(Resource res) {
this.res = res;
}
public void run() {
while (true) {
res.set(".商品.");
}
}
}
// 消费者线程
public class Consumer implements Runnable {
private Resource res;
Consumer(Resource res) {
this.res = res;
}
public void run() {
while (true) {
res.out();
}
}
}
// 主方法
public class ProducerConsumerMain {
public static void main(String[] args) {
// 创建商品库,用来存商品和消费商品
Resource r = new Resource();
// 启动生产、消费两个线程
new Thread(new Producer(r)).start();
new Thread(new Consumer(r)).start();
}
}
多生产者-多消费者问题
问题: 上面的示例只适合一个生产线程和一个消费线程。如果创建多个生产线程和多个消费线程,系统将无法保证生产一次便消费一次。
解决方案:
- 将标记判断
if改为while让其循环判断 - 将
notify()换为notifyAll(),唤醒所有等待的线程
问题: 使用notifyAll()会唤醒所有等待线程,消耗更多系统资源。
更好的解决方案: JDK 1.5后的Lock、Condition接口
5.4 Lock、Condition接口(JDK 1.5+)
改进要点:
synchronized→Lock显示锁操作wait/notify/notifyAll→Condition的await/signal/signalAll- Lock支持创建多个相关的Condition对象
- 可以精确控制哪些线程被唤醒
核心接口说明
| 传统方式 | Lock方式 | 说明 |
|---|---|---|
synchronized |
Lock.lock() / Lock.unlock() |
显式锁操作 |
wait() |
Condition.await() |
线程等待 |
notify() |
Condition.signal() |
唤醒一个线程 |
notifyAll() |
Condition.signalAll() |
唤醒所有线程 |
Lock版本的生产者-消费者实现
java
import java.util.concurrent.locks.*;
// 使用Lock、Condition接口的Resource类
public class Resource {
private String name;
private int count = 1;
private boolean flag = false;
// 应用Lock、Condition接口
private Lock lock = new ReentrantLock();
// 创建两个相关Condition对象
private Condition condition_pro = lock.newCondition(); // 生产者等待条件
private Condition condition_con = lock.newCondition(); // 消费者等待条件
// 生产操作
public void set(String name) {
lock.lock();
try {
while (flag) {
// 生产线程等待
condition_pro.await();
}
this.name = name + "..." + count++;
System.out.println(Thread.currentThread().getName() +
"..生产。。" + this.name);
// 改变标记,好让消费线程成功运行,true代表可消费
flag = true;
// 精确唤醒消费线程
condition_con.signal();
} catch(InterruptedException e) {
} finally {
lock.unlock();
}
}
// 消费操作
public void out() {
lock.lock();
try {
while (!flag) {
// 消费线程等待
condition_con.await();
}
System.out.println(Thread.currentThread().getName() +
"。消费。" + this.name);
// 改变标记,生产线程false代表可生产
flag = false;
// 精确唤醒生产线程
condition_pro.signal();
} catch(InterruptedException e) {
} finally {
lock.unlock();
}
}
}
优势:
- 精确唤醒:可以指定唤醒特定类型的线程
- 资源节省:避免唤醒不必要的线程
- 程序灵活性:支持更复杂的线程协调场景
6. 线程的其他重要方法
6.1 线程控制方法
| 方法名 | 功能描述 | 使用说明 |
|---|---|---|
stop() |
停止线程 | 已过时,现在通过控制run()方法结束来停止线程 |
interrupt() |
中断线程 | 清除冻结状态,强制让线程恢复到运行状态 |
setDaemon(boolean on) |
设置守护线程 | 设为true时,主线程结束,守护线程也自动结束 |
join() |
等待线程结束 | 当前线程放弃执行权,等待调用join()的线程执行完 |
方法详细说明
1. interrupt() 方法
java
// 中断线程示例
public class InterruptDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
System.out.println("线程运行中...");
} catch (InterruptedException e) {
System.out.println("线程被中断");
Thread.currentThread().interrupt(); // 重新设置中断标志
break;
}
}
});
t.start();
// 3秒后中断线程
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
}
2. join() 方法
java
// join方法示例
public class JoinDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("t1: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
try {
t1.join(); // 主线程等待t1执行完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程继续执行");
}
}
6.2 线程信息方法
| 方法名 | 功能描述 | 返回值 |
|---|---|---|
toString() |
获取线程信息 | 线程名、优先级、线程组 |
setPriority(int newPriority) |
设置线程优先级 | void |
getPriority() |
获取线程优先级 | int |
yield() |
线程让步 | void |
线程优先级
优先级常量:
Thread.MIN_PRIORITY = 1(最低优先级)Thread.NORM_PRIORITY = 5(普通优先级,默认)Thread.MAX_PRIORITY = 10(最高优先级)
java
// 线程优先级示例
public class PriorityDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("低优先级线程: " + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("高优先级线程: " + i);
}
});
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
3. yield() 方法
java
// yield方法示例
public class YieldDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("线程1: " + i);
if (i == 5) {
Thread.yield(); // 让步给其他线程
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("线程2: " + i);
}
});
t1.start();
t2.start();
}
}
7. 线程池(高级主题)
7.1 线程池概述
线程池的优势:
- 降低资源消耗:重复利用已创建的线程
- 提高响应速度:任务到达时不需要等待线程创建
- 提高线程的可管理性:统一分配、调优和监控
7.2 线程池的使用
java
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("任务 " + taskId + " 在线程 " +
Thread.currentThread().getName() + " 中执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
7.3 常用线程池类型
| 线程池类型 | 创建方式 | 特点 |
|---|---|---|
| 固定大小线程池 | newFixedThreadPool(n) |
线程数固定为n |
| 缓存线程池 | newCachedThreadPool() |
线程数根据需要动态创建 |
| 单线程线程池 | newSingleThreadExecutor() |
只有一个工作线程 |
| 调度线程池 | newScheduledThreadPool(n) |
支持定时和周期性任务 |
8. 常见异常处理
8.1 多线程相关异常
| 异常名称 | 异常描述 | 常见原因 | 处理建议 |
|---|---|---|---|
IllegalThreadStateException |
线程状态不当异常 | 线程没有处于操作所需的适当状态 | 检查线程状态再进行操作 |
InterruptedException |
线程中断异常 | 线程在等待、休眠或暂停状态被其他线程中断 | 捕获异常并适当处理中断 |
SecurityException |
安全异常 | 当前线程无权修改目标线程 | 检查安全管理器设置 |
8.2 异常处理示例
java
public class ThreadExceptionDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
// 模拟工作
Thread.sleep(1000);
System.out.println("工作中...");
}
} catch (InterruptedException e) {
System.out.println("线程被中断: " + e.getMessage());
// 处理中断
Thread.currentThread().interrupt();
}
});
try {
t.start();
Thread.sleep(3000);
t.interrupt(); // 中断线程
} catch (InterruptedException e) {
System.out.println("主线程被中断: " + e.getMessage());
}
}
}
9. 多线程最佳实践
9.1 线程安全编程原则
- 最小化共享数据:尽量减少线程间共享的数据
- 使用不可变对象:不可变对象天然线程安全
- 正确使用同步:避免过度同步和同步不足
- 避免死锁:注意锁的获取顺序
9.2 性能优化建议
- 选择合适的创建方式:优先使用实现Runnable接口
- 使用线程池:避免频繁创建和销毁线程
- 合理设置线程优先级:避免优先级倒置问题
- 及时释放资源:确保锁能够正确释放
9.3 调试多线程程序
java
public class ThreadDebugDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("线程名称: " + Thread.currentThread().getName());
System.out.println("线程ID: " + Thread.currentThread().getId());
System.out.println("线程优先级: " + Thread.currentThread().getPriority());
System.out.println("是否为守护线程: " + Thread.currentThread().isDaemon());
}, "调试线程");
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束");
}
}
10. 总结
多线程的核心概念
- 线程创建:实现Runnable接口是推荐方式
- 线程安全:使用同步机制保护共享资源
- 线程通信:wait/notify或Lock/Condition实现线程协作
- 线程控制:合理使用各种线程控制方法
开发建议
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 简单并行任务 | 实现Runnable接口 | 避免继承限制 |
| 复杂线程协作 | Lock + Condition | 更精确的控制 |
| 大量短期任务 | 线程池 | 提高性能和可管理性 |
| 定时任务 | ScheduledExecutorService | 专业的调度功能 |
注意事项
- 避免滥用synchronized:过度同步会降低性能
- 正确处理InterruptedException:不要忽略线程中断
- 谨慎使用stop()方法:已过时,可能导致数据不一致
- 合理设计线程间通信:使用合适的同步机制
多线程编程是Java开发中的重要技能,正确理解和使用多线程可以显著提高程序的性能和用户体验。但同时也要注意线程安全问题,避免出现竞态条件和死锁等问题。
本文全面介绍了Java多线程编程的核心概念、实现方式、同步机制和最佳实践,希望对深入理解和掌握Java多线程技术有所帮助。