1. Java中使用多线程的方式
主要有以下几种方式:
- 继承Thread类
class MyThread extends Thread {
public void run() {
// 线程执行代码
}
}
- 实现Runnable接口
class MyRunnable implements Runnable {
public void run() {
// 线程执行代码
}
}
- 实现Callable接口
class MyCallable implements Callable<String> {
public String call() throws Exception {
return "结果";
}
}
- 使用线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(new MyRunnable());
2. 线程的几种状态
线程状态流程图:
新建(NEW) -> 就绪(RUNNABLE) -> 运行(RUNNING)
↑ ↓
└── 阻塞(BLOCKED) ←┘
└── 等待(WAITING) ←┘
└── 死亡(TERMINATED)
- NEW: 线程被创建,还未调用start()
- RUNNABLE: 可运行状态,等待CPU调度
- BLOCKED: 阻塞状态,等待锁
- WAITING: 等待状态,等待其他线程通知
- TIMED_WAITING: 超时等待状态
- TERMINATED: 终止状态
我来绘制一个更详细的 Java 线程状态流程图,包含状态转换的具体触发条件:
graph TD
A["新建状态 NEW"] --> |"调用 start()"| B["就绪状态 RUNNABLE"]
B --> |"获得 CPU 时间片"| C["运行状态 RUNNING"]
C --> |"失去 CPU 时间片"| B
C --> |"调用 sleep()"| D["计时等待 TIMED_WAITING"]
D --> |"时间到期"| B
C --> |"调用 wait()"| E["等待状态 WAITING"]
E --> |"notify()/notifyAll()"| B
C --> |"等待进入同步代码块/方法"| F["阻塞状态 BLOCKED"]
F --> |"获得锁"| B
C --> |"run() 执行完毕"| G["终止状态 TERMINATED"]
C --> |"join()"| E
C --> |"wait(time)"| D
C --> |"LockSupport.park()"| E
E --> |"LockSupport.unpark()"| B
详细说明各状态之间的转换:
-
NEW(新建)→ RUNNABLE(就绪)
- 调用 thread.start() 方法
-
RUNNABLE(就绪)↔ RUNNING(运行)
- 获得 CPU 时间片进入 RUNNING
- 时间片用完返回 RUNNABLE
-
RUNNING(运行)→ WAITING(等待)
- 调用 object.wait()
- 调用 thread.join()
- 调用 LockSupport.park()
-
WAITING(等待)→ RUNNABLE(就绪)
- 调用 object.notify()/notifyAll()
- 等待的线程执行完毕(join 的情况)
- 调用 LockSupport.unpark()
-
RUNNING(运行)→ TIMED_WAITING(限时等待)
- 调用 thread.sleep(time)
- 调用 object.wait(time)
- 调用 thread.join(time)
-
TIMED_WAITING(限时等待)→ RUNNABLE(就绪)
- 等待时间结束
- 调用 notify()/notifyAll()
-
RUNNING(运行)→ BLOCKED(阻塞)
- 等待进入 synchronized 同步块/方法
- 等待获取锁
-
BLOCKED(阻塞)→ RUNNABLE(就绪)
- 获得锁
-
RUNNING(运行)→ TERMINATED(终止)
- run() 方法执行完成
- 出现未捕获的异常
关键点说明:
- RUNNABLE 状态包含了就绪和运行两个状态,Java 将这两个状态合并为 RUNNABLE
- 线程从 NEW 状态只能转换到 RUNNABLE 状态,不能逆转
- 线程一旦进入 TERMINATED 状态,就不能再转换为其他状态
- BLOCKED、WAITING、TIMED_WAITING 都是阻塞状态,只是阻塞的原因不同
- 任何一个阻塞状态的线程被唤醒后,都需要先进入 RUNNABLE 状态,等待获取 CPU 时间片
这个流程图展示了 Java 线程从创建到销毁的完整生命周期,以及在各个状态之间可能发生的转换条件。理解这些状态转换对于进行多线程编程和调试非常重要。
标准回答模板
"Java 线程状态这个问题,我从三个方面来回答:基本状态、状态转换、以及实际应用。
第一,基本状态 Java 线程在 JVM 中定义了六种基本状态:
- NEW(新建)- 线程创建但还未启动
- RUNNABLE(可运行)- 包括正在运行和等待 CPU 调度
- BLOCKED(阻塞)- 等待获取锁
- WAITING(等待)- 无限期等待其他线程的操作
- TIMED_WAITING(限时等待)- 有时限的等待
- TERMINATED(终止)- 线程执行完毕
第二,关键的状态转换 我重点说几个最常见的转换场景:
-
从 NEW 到 RUNNABLE:
- 调用 thread.start() 方法
-
从 RUNNABLE 到 BLOCKED:
- 等待进入 synchronized 同步块
- 等待获取锁
-
从 RUNNABLE 到 WAITING:
- 调用 object.wait()
- 调用 thread.join()
- 调用 LockSupport.park()
-
从 RUNNABLE 到 TIMED_WAITING:
- 调用 Thread.sleep(time)
- 调用 object.wait(time)
第三,实际应用 在实际开发中,理解线程状态对排查问题很有帮助:
- 如果发现大量线程在 BLOCKED 状态,可能存在锁竞争激烈的问题
- 如果发现线程长期处于 WAITING 状态,可能是忘记了 notify/notifyAll 调用
- 使用 jstack 命令可以查看线程状态,帮助定位死锁或性能问题
补充说明
如果面试官继续追问,我还可以补充:
-
线程状态的监控方式:
- 可以通过 JMX
- jstack 命令
- Thread.getState() 方法获取
-
常见问题的解决方案:
- 对于 BLOCKED 状态过多,可以考虑优化锁粒度
- 对于 WAITING 状态异常,可以添加超时机制
- 使用线程池来管理线程生命周期"
3. 多线程同步的实现方式
- synchronized关键字
synchronized void method() {
// 同步代码
}
- ReentrantLock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 同步代码
} finally {
lock.unlock();
}
- volatile关键字
private volatile boolean flag = false;
让我用简单的比喻和流程图来解释这三种同步方式的区别:
通俗解释
- synchronized(内置锁)
- 就像一个带锁的房间
- 一次只能有一个人进入
- 进入时自动上锁,离开时自动解锁
- 适用场景:单个方法或代码块需要同步
- ReentrantLock(可重入锁)
- 像一个带钥匙的房间
- 可以手动上锁解锁
- 支持更多高级功能(等待中断、限时等待、公平锁)
- 适用场景:需要灵活控制加锁解锁的场合
- volatile(内存可见性)
- 像一个透明的公告板
- 所有人都能立即看到最新的内容
- 不能保证复合操作的原子性
- 适用场景:一个线程写,多个线程读的场合
流程图
flowchart TD
%% synchronized 部分
S1[synchronized] --> S2[自动加锁/解锁]
S2 --> S3[不可中断]
S2 --> S4[不可设置超时]
S2 --> S5[不可设置公平性]
%% ReentrantLock 部分
R1[ReentrantLock] --> R2[手动加锁/解锁]
R2 --> R3[可中断]
R2 --> R4[可设置超时]
R2 --> R5[可设置公平性]
%% volatile 部分
V1[volatile] --> V2[内存可见性]
V2 --> V3[不保证原子性]
V2 --> V4[禁止指令重排]
详细对比
| 特性 | synchronized | ReentrantLock | volatile |
|---|---|---|---|
| 实现方式 | JVM层面 | API层面 | JVM层面 |
| 使用难度 | 简单 | 相对复杂 | 简单 |
| 灵活性 | 低 | 高 | 低 |
| 性能 | 一般 | 较好 | 最好 |
| 功能 | 基础同步 | 高级同步 | 可见性保证 |
代码示例
- synchronized 示例
public class SynchronizedExample {
public synchronized void method() {
// 同步方法
}
public void blockMethod() {
synchronized(this) {
// 同步代码块
}
}
}
- ReentrantLock 示例
public class ReentrantLockExample {
private ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 同步代码
} finally {
lock.unlock(); // 必须在 finally 中释放锁
}
}
}
- volatile 示例
public class VolatileExample {
private volatile boolean flag = false;
public void write() {
flag = true; // 写操作立即对其他线程可见
}
public void read() {
while (!flag) {
// 能立即看到其他线程对 flag 的修改
}
}
}
使用建议
-
使用 synchronized 当:
- 需要简单的同步
- 不需要高级特性
- 代码简洁性要求高
-
使用 ReentrantLock 当:
- 需要尝试获取锁
- 需要可中断的锁获取
- 需要公平锁
- 需要多个等待队列
-
使用 volatile 当:
- 只需要保证变量可见性
- 一写多读的场景
- 不需要保证原子性
注意事项
-
synchronized:
- 自动释放锁
- 可能产生死锁
- 不能中断一个正在等待获取锁的线程
-
ReentrantLock:
- 必须手动释放锁
- 可以避免死锁
- 提供更多控制机制
-
volatile:
- 不保证原子性
- 只保证可见性和有序性
- 性能好但功能有限
这三种同步机制各有特点,在实际开发中要根据具体场景选择合适的方式。一般来说,如果不需要 ReentrantLock 的特殊功能,优先使用 synchronized;如果只需要保证可见性,则考虑 volatile。
4. 线程死锁及避免
死锁产生的四个必要条件:
- 互斥条件
- 请求与保持条件
- 不剥夺条件
- 循环等待条件
避免死锁的方法:
- 固定加锁顺序
- 设置超时时间
- 使用tryLock()方法
- 避免嵌套锁
让我用更通俗的方式来讲解线程死锁,并配上流程图。
🎭 什么是死锁?- 生活场景类比
想象一个十字路口:
- 甲车在南北方向等待,想向北行驶
- 乙车在东西方向等待,想向东行驶
- 两辆车都开到了路口中间
- 甲车说:"你后退我才能过"
- 乙车说:"你后退我才能过"
- 结果:两车都动弹不得!
🔄 死锁的四个必要条件
graph TD
A[互斥条件] --> E[死锁产生]
B[请求与保持] --> E
C[不可剥夺] --> E
D[循环等待] --> E
用餐厅场景解释这四个条件:
- 互斥条件
- 一双筷子同一时刻只能被一个人使用
- 请求与保持
- 小明拿着左边筷子,还想要右边的筷子
- 不可剥夺
- 除非小明主动放下,否则别人不能抢走他的筷子
- 循环等待
- 每个人都拿着左边的筷子,等待右边的筷子
🎯 如何避免死锁?
graph LR
A[预防死锁] --> B[破坏互斥]
A --> C[破坏请求保持]
A --> D[破坏不可剥夺]
A --> E[破坏循环等待]
📝 实际编码示例
- 错误示例 - 可能导致死锁
public class DeadLockExample {
public void method1() {
synchronized(资源A) {
synchronized(资源B) {
// 业务代码
}
}
}
public void method2() {
synchronized(资源B) { // 注意这里顺序相反
synchronized(资源A) {
// 业务代码
}
}
}
}
- 正确示例 - 避免死锁
public class SafeLockExample {
private static final Object LOCK_1 = new Object();
private static final Object LOCK_2 = new Object();
public void safeMethod() {
// 始终按照固定顺序获取锁
synchronized(LOCK_1) {
synchronized(LOCK_2) {
// 业务代码
}
}
}
}
🎨 死锁避免策略流程图
flowchart TD
A[开始] --> B{是否需要多个锁?}
B -->|是| C[使用有序加锁]
B -->|否| D[使用单锁]
C --> E{是否可能长时间持有锁?}
E -->|是| F[设置锁超时]
E -->|否| G[使用 synchronized]
F --> H[使用 tryLock]
G --> I[结束]
H --> I
D --> I
根据死锁避免策略流程图编写一个完整的示例代码,展示不同场景下如何正确处理锁。
以转账业务为例,演示不同场景的处理方式:
public class BankDemo {
// 账户类
static class Account {
private final String accountNo;
private double balance;
private final Lock lock = new ReentrantLock();
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
}
// 1. 单锁场景:查询余额
public double getBalance(Account account) {
// 场景:单锁,使用 synchronized
synchronized (account) {
return account.balance;
}
}
// 2. 多锁场景(短时间):小额转账
public boolean transferSmallAmount(Account from, Account to, double amount) {
// 场景:多锁,短时间操作,使用有序加锁
Account firstLock = from.getAccountNo().compareTo(to.getAccountNo()) < 0 ? from : to;
Account secondLock = from.getAccountNo().compareTo(to.getAccountNo()) < 0 ? to : from;
synchronized (firstLock) {
synchronized (secondLock) {
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
return true;
}
return false;
}
}
}
// 3. 多锁场景(长时间):大额转账,需要额外验证
public boolean transferLargeAmount(Account from, Account to, double amount) {
// 场景:多锁,长时间操作,使用 tryLock 带超时
long timeout = 3000; // 3秒超时
long deadline = System.currentTimeMillis() + timeout;
while (true) {
// 尝试获取第一个锁
if (from.lock.tryLock()) {
try {
// 尝试获取第二个锁
if (to.lock.tryLock()) {
try {
// 验证余额
if (from.balance < amount) {
return false;
}
// 模拟对大额转账的额外验证
try {
Thread.sleep(1000); // 模拟验证过程
} catch (InterruptedException e) {
return false;
}
// 执行转账
from.balance -= amount;
to.balance += amount;
return true;
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock();
}
}
// 检查是否超时
if (System.currentTimeMillis() > deadline) {
return false;
}
// 短暂休眠后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
return false;
}
}
}
// 4. 使用示例
public static void main(String[] args) {
BankDemo bank = new BankDemo();
Account accountA = new Account("A", 1000);
Account accountB = new Account("B", 1000);
// 创建多个线程模拟并发操作
ExecutorService executor = Executors.newFixedThreadPool(3);
// 模拟查询操作
executor.submit(() -> {
double balance = bank.getBalance(accountA);
System.out.println("账户A余额: " + balance);
});
// 模拟小额转账
executor.submit(() -> {
boolean success = bank.transferSmallAmount(accountA, accountB, 100);
System.out.println("小额转账" + (success ? "成功" : "失败"));
});
// 模拟大额转账
executor.submit(() -> {
boolean success = bank.transferLargeAmount(accountA, accountB, 500);
System.out.println("大额转账" + (success ? "成功" : "失败"));
});
executor.shutdown();
}
}
代码说明:
-
单锁场景
getBalance方法使用简单的synchronized锁- 适用于简单的原子操作
-
多锁短时间场景
transferSmallAmount方法使用有序加锁策略- 通过比较账号确保加锁顺序一致
- 适用于简单的多资源操作
-
多锁长时间场景
transferLargeAmount方法使用tryLock带超时机制- 包含重试逻辑
- 适用于复杂的多资源操作
-
最佳实践
- 使用线程池控制并发
- 合理设置超时时间
- 正确释放锁资源
- 优雅处理异常情况
让我详细解释这两行代码的作用。
顺序加锁的代码说明:Account firstLock = from.getAccountNo().compareTo(to.getAccountNo()) < 0 ? from : to; Account secondLock = from.getAccountNo().compareTo(to.getAccountNo()) < 0 ? to : from;
Account firstLock = from.getAccountNo().compareTo(to.getAccountNo()) < 0 ? from : to;
Account secondLock = from.getAccountNo().compareTo(to.getAccountNo()) < 0 ? to : from;
这是使用三元运算符来确定加锁顺序的代码。让我们逐步分解:
-
compareTo()方法- 这是字符串比较方法
- 返回值:
- 小于0:第一个字符串小于第二个
- 等于0:两个字符串相等
- 大于0:第一个字符串大于第二个
-
举个例子:
// 假设有两个账号
Account accountA = new Account("A123", 1000); // 账号A123
Account accountB = new Account("B456", 1000); // 账号B456
// 比较账号
"A123".compareTo("B456") < 0 // 结果为true,因为A在B之前
- 代码执行流程:
// 情况1:from="A123", to="B456"
from.getAccountNo().compareTo(to.getAccountNo()) < 0 // true
firstLock = from // A123账户
secondLock = to // B456账户
// 情况2:from="B456", to="A123"
from.getAccountNo().compareTo(to.getAccountNo()) < 0 // false
firstLock = to // A123账户
secondLock = from // B456账户
为什么要这样做?
-
防止死锁的关键:确保所有线程按照相同的顺序获取锁
-
图解说明:
graph TD
A[比较账号] --> B{A123 < B456 ?}
B -->|是| C[先锁A123,后锁B456]
B -->|否| D[先锁B456,后锁A123]
- 实际场景示例:
// 线程1:从A转账到B
public void transfer1() {
// 不管是A转B还是B转A
// 都会先锁住号码小的账户
Account firstLock = from.getAccountNo().compareTo(to.getAccountNo()) < 0 ? from : to;
Account secondLock = from.getAccountNo().compareTo(to.getAccountNo()) < 0 ? to : from;
synchronized(firstLock) {
synchronized(secondLock) {
// 转账操作
}
}
}
// 线程2:从B转账到A
public void transfer2() {
// 同样的逻辑确保顺序一致
Account firstLock = from.getAccountNo().compareTo(to.getAccountNo()) < 0 ? from : to;
Account secondLock = from.getAccountNo().compareTo(to.getAccountNo()) < 0 ? to : from;
synchronized(firstLock) {
synchronized(secondLock) {
// 转账操作
}
}
}
不这样做会怎样?
// 错误示例
public void transfer1() {
synchronized(accountA) { // 先锁A
synchronized(accountB) { // 再锁B
// 转账操作
}
}
}
public void transfer2() {
synchronized(accountB) { // 先锁B
synchronized(accountA) { // 再锁A
// 转账操作
}
}
}
这样可能导致:
- 线程1持有A的锁,等待B的锁
- 线程2持有B的锁,等待A的锁
- 形成死锁!
总结
-
这两行代码的目的是:
- 确保所有线程按照相同的顺序获取锁
- 通过比较账号大小来决定加锁顺序
- 防止死锁发生
-
核心原则:
- 总是先锁账号小的
- 再锁账号大的
- 保证顺序一致性
这种方式可以有效防止死锁,是一种常用的死锁预防策略。
使用建议:
- 优先使用单锁
- 必须多锁时,注意加锁顺序
- 长时间操作使用超时机制
- 合理使用线程池
- 始终在 finally 块中释放锁
这个例子展示了如何根据不同场景选择合适的锁策略,有效避免死锁问题。实际应用中,还需要根据具体业务需求调整超时时间、重试策略等参数。
-
资源有序分配
- 给资源编号
- 按照固定顺序申请资源
-
超时机制
// 使用带超时的锁获取
Lock lock = new ReentrantLock();
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
- 使用并发工具
// 使用线程池避免过多线程
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(() -> {
// 任务执行
});
📊 死锁检测与恢复
graph TD
A[检测到死锁] --> B{是否可以回退?}
B -->|可以| C[回退到安全状态]
B -->|不可以| D[强制终止一个进程]
C --> E[继续执行]
D --> E
🔍 死锁检测与恢复流程图
graph TD
A[开始监控] --> B[检测是否存在死锁]
B --> C{发现死锁?}
C -->|是| D[选择牺牲者进程]
C -->|否| E[继续监控]
D --> F[回滚事务]
F --> G[释放资源]
G --> B
💻 代码示例
public class DeadlockDetectionDemo {
// 用于记录资源分配情况的数据结构
static class ResourceManager {
// 存储进程持有的资源
private Map<String, Set<String>> holdingResources = new HashMap<>();
// 存储进程等待的资源
private Map<String, Set<String>> waitingResources = new HashMap<>();
// 记录转账事务
private Map<String, TransferTransaction> transactions = new HashMap<>();
// 转账事务类
static class TransferTransaction {
String fromAccount;
String toAccount;
double amount;
long startTime;
public TransferTransaction(String from, String to, double amount) {
this.fromAccount = from;
this.toAccount = to;
this.amount = amount;
this.startTime = System.currentTimeMillis();
}
}
// 添加资源占用记录
public synchronized void addResourceHold(String processId, String resource) {
holdingResources.computeIfAbsent(processId, k -> new HashSet<>()).add(resource);
}
// 添加资源等待记录
public synchronized void addResourceWait(String processId, String resource) {
waitingResources.computeIfAbsent(processId, k -> new HashSet<>()).add(resource);
}
// 检测死锁
public synchronized List<String> detectDeadlock() {
List<String> deadlockedProcesses = new ArrayList<>();
// 构建等待图
Map<String, Set<String>> waitGraph = new HashMap<>();
for (Map.Entry<String, Set<String>> entry : waitingResources.entrySet()) {
String processId = entry.getKey();
Set<String> waitingRes = entry.getValue();
for (String res : waitingRes) {
// 找到持有该资源的进程
for (Map.Entry<String, Set<String>> holder : holdingResources.entrySet()) {
if (holder.getValue().contains(res)) {
// 建立进程间的等待关系
waitGraph.computeIfAbsent(processId, k -> new HashSet<>())
.add(holder.getKey());
}
}
}
}
// 检测循环等待
for (String processId : waitGraph.keySet()) {
Set<String> visited = new HashSet<>();
if (hasCycle(processId, waitGraph, visited)) {
deadlockedProcesses.add(processId);
}
}
return deadlockedProcesses;
}
// 检测循环等待
private boolean hasCycle(String current, Map<String, Set<String>> waitGraph,
Set<String> visited) {
if (visited.contains(current)) {
return true;
}
visited.add(current);
Set<String> waiting = waitGraph.get(current);
if (waiting != null) {
for (String next : waiting) {
if (hasCycle(next, waitGraph, visited)) {
return true;
}
}
}
visited.remove(current);
return false;
}
// 选择要回滚的事务
public synchronized String selectVictim(List<String> deadlockedProcesses) {
String victim = null;
long oldestStartTime = Long.MAX_VALUE;
// 选择运行时间最长的事务作为牺牲者
for (String processId : deadlockedProcesses) {
TransferTransaction transaction = transactions.get(processId);
if (transaction.startTime < oldestStartTime) {
oldestStartTime = transaction.startTime;
victim = processId;
}
}
return victim;
}
// 回滚事务
public synchronized void rollbackTransaction(String processId) {
// 释放所有持有的资源
holdingResources.remove(processId);
waitingResources.remove(processId);
TransferTransaction tx = transactions.remove(processId);
System.out.println("回滚事务: " + processId +
" (从 " + tx.fromAccount + " 到 " + tx.toAccount +
" 转账 " + tx.amount + ")");
}
}
// 演示使用
public static void main(String[] args) {
ResourceManager resourceManager = new ResourceManager();
// 模拟两个并发转账操作
String tx1 = "TX1";
String tx2 = "TX2";
// 记录事务信息
resourceManager.transactions.put(tx1,
new ResourceManager.TransferTransaction("A", "B", 100));
resourceManager.transactions.put(tx2,
new ResourceManager.TransferTransaction("B", "A", 50));
// 模拟资源分配
resourceManager.addResourceHold(tx1, "AccountA");
resourceManager.addResourceWait(tx1, "AccountB");
resourceManager.addResourceHold(tx2, "AccountB");
resourceManager.addResourceWait(tx2, "AccountA");
// 死锁检测
System.out.println("开始死锁检测...");
List<String> deadlockedProcesses = resourceManager.detectDeadlock();
if (!deadlockedProcesses.isEmpty()) {
System.out.println("检测到死锁!涉及的事务: " + deadlockedProcesses);
// 选择牺牲者
String victim = resourceManager.selectVictim(deadlockedProcesses);
System.out.println("选择回滚事务: " + victim);
// 回滚事务
resourceManager.rollbackTransaction(victim);
} else {
System.out.println("未检测到死锁");
}
}
}
🎯 代码执行流程说明
-
资源管理
- 使用
holdingResources记录每个进程持有的资源 - 使用
waitingResources记录每个进程等待的资源 - 使用
transactions记录转账事务信息
- 使用
-
死锁检测步骤
graph LR A[构建等待图] --> B[检测循环等待] B --> C{存在死锁?} C -->|是| D[选择牺牲者] C -->|否| E[继续执行] D --> F[回滚事务] -
选择牺牲者策略
- 选择运行时间最长的事务
- 可以根据实际需求调整策略,比如:
- 事务优先级
- 回滚成本
- 资源占用量
-
回滚操作
- 释放所有持有的资源
- 清除等待记录
- 回滚事务操作
📝 运行结果示例
开始死锁检测...
检测到死锁!涉及的事务: [TX1, TX2]
选择回滚事务: TX1
回滚事务: TX1 (从 A 到 B 转账 100)
📝 什么是等待图
好的,我来详细解释构建等待图的过程。让我们用一个具体的场景来说明。
1. 什么是等待图?
等待图是一个表示进程之间资源等待关系的有向图:
- 节点代表进程
- 箭头表示等待关系(A → B 表示A在等待B持有的资源)
2. 具体示例
假设有这样一个场景:
- 进程1持有资源A,等待资源B
- 进程2持有资源B,等待资源A
graph LR
Process1 -->|等待| Process2
Process2 -->|等待| Process1
3. 代码详解
// 构建等待图的代码
public synchronized List<String> detectDeadlock() {
// waitGraph用来存储进程间的等待关系
Map<String, Set<String>> waitGraph = new HashMap<>();
// 示例数据
/*
holdingResources = {
"Process1": ["ResourceA"],
"Process2": ["ResourceB"]
}
waitingResources = {
"Process1": ["ResourceB"],
"Process2": ["ResourceA"]
}
*/
// 遍历每个正在等待资源的进程
for (Map.Entry<String, Set<String>> entry : waitingResources.entrySet()) {
String processId = entry.getKey(); // 当前进程
Set<String> waitingRes = entry.getValue(); // 该进程等待的资源
// 对于该进程等待的每个资源
for (String res : waitingRes) {
// 查找谁持有这个资源
for (Map.Entry<String, Set<String>> holder : holdingResources.entrySet()) {
String holderProcessId = holder.getKey(); // 持有资源的进程
Set<String> holdingRes = holder.getValue(); // 该进程持有的资源
if (holdingRes.contains(res)) {
// 建立等待关系:当前进程 -> 持有资源的进程
waitGraph.computeIfAbsent(processId, k -> new HashSet<>())
.add(holderProcessId);
}
}
}
}
return checkForDeadlock(waitGraph);
}
4. 图解构建过程
假设有如下场景:
// 初始状态
ResourceManager rm = new ResourceManager();
// Process1 持有 ResourceA,等待 ResourceB
rm.addResourceHold("Process1", "ResourceA");
rm.addResourceWait("Process1", "ResourceB");
// Process2 持有 ResourceB,等待 ResourceA
rm.addResourceHold("Process2", "ResourceB");
rm.addResourceWait("Process2", "ResourceA");
构建过程:
- 第一步:处理Process1
graph TD
subgraph 查找Process1等待的ResourceB
P1[Process1] -->|等待ResourceB| P2[Process2]
end
- 第二步:处理Process2
graph TD
subgraph 查找Process2等待的ResourceA
P1[Process1] -->|等待ResourceB| P2[Process2]
P2 -->|等待ResourceA| P1
end
5. 代码示例(带详细注释)
public class DeadlockDetectionExample {
public static void main(String[] args) {
ResourceManager rm = new ResourceManager();
// 模拟场景:两个进程相互等待对方的资源
// Process1的资源状态
rm.addResourceHold("Process1", "ResourceA"); // Process1持有资源A
rm.addResourceWait("Process1", "ResourceB"); // Process1等待资源B
// Process2的资源状态
rm.addResourceHold("Process2", "ResourceB"); // Process2持有资源B
rm.addResourceWait("Process2", "ResourceA"); // Process2等待资源A
// 打印当前资源分配状态
System.out.println("资源持有情况:");
System.out.println(rm.holdingResources);
System.out.println("资源等待情况:");
System.out.println(rm.waitingResources);
// 构建等待图并检测死锁
Map<String, Set<String>> waitGraph = new HashMap<>();
// 遍历等待资源的进程
for (Map.Entry<String, Set<String>> entry : rm.waitingResources.entrySet()) {
String processId = entry.getKey();
Set<String> waitingRes = entry.getValue();
System.out.println("\n处理进程: " + processId);
System.out.println("该进程等待的资源: " + waitingRes);
// 对于每个等待的资源
for (String res : waitingRes) {
System.out.println("查找资源 " + res + " 的持有者");
// 查找谁持有这个资源
for (Map.Entry<String, Set<String>> holder : rm.holdingResources.entrySet()) {
String holderProcessId = holder.getKey();
Set<String> holdingRes = holder.getValue();
if (holdingRes.contains(res)) {
System.out.println("发现 " + res + " 被进程 " + holderProcessId + " 持有");
// 建立等待关系
waitGraph.computeIfAbsent(processId, k -> new HashSet<>())
.add(holderProcessId);
}
}
}
}
System.out.println("\n最终等待图:");
waitGraph.forEach((process, waiting) -> {
System.out.println(process + " 等待 " + waiting);
});
}
}
6. 运行结果
资源持有情况:
{Process1=[ResourceA], Process2=[ResourceB]}
资源等待情况:
{Process1=[ResourceB], Process2=[ResourceA]}
处理进程: Process1
该进程等待的资源: [ResourceB]
查找资源 ResourceB 的持有者
发现 ResourceB 被进程 Process2 持有
处理进程: Process2
该进程等待的资源: [ResourceA]
查找资源 ResourceA 的持有者
发现 ResourceA 被进程 Process1 持有
最终等待图:
Process1 等待 [Process2]
Process2 等待 [Process1]
这样的等待图清晰地显示了进程之间的循环等待关系,帮助我们发现死锁。当发现等待图中存在环(循环)时,就表示存在死锁。在这个例子中,Process1等待Process2,Process2又等待Process1,形成了一个环,因此存在死锁。
💡 实际应用建议
- 定期检测
// 创建定时检测任务
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
List<String> deadlocked = resourceManager.detectDeadlock();
if (!deadlocked.isEmpty()) {
// 处理死锁
}
}, 0, 5, TimeUnit.SECONDS);
- 设置超时机制
if (System.currentTimeMillis() - transaction.startTime > TIMEOUT_THRESHOLD) {
resourceManager.rollbackTransaction(processId);
}
- 记录日志
logger.info("检测到死锁,涉及事务: {}", deadlockedProcesses);
logger.info("回滚事务: {}", victim);
这个示例展示了一个基本的死锁检测与恢复机制。在实际应用中,还需要考虑:
- 分布式环境下的死锁检测
- 更复杂的资源分配策略
- 更完善的回滚机制
- 性能优化等问题
🎯 总结要点
-
预防措施:
- 固定顺序申请资源
- 使用超时机制
- 避免嵌套锁
- 使用并发工具类
-
实现技巧:
- 使用
tryLock() - 合理设置超时时间
- 资源编号排序
- 及时释放资源
- 使用
-
最佳实践:
- 减少锁的持有时间
- 避免事务过长
- 使用线程池
- 定期检测死锁
记住:预防胜于治疗,在设计阶段就要考虑到死锁的可能性!
5. 线程阻塞的原因
好的,让我用生活中的例子来形象解释线程阻塞的各种情况。
1. 线程阻塞就像生活中的排队
想象你在一个美食广场:
- 排队买饭 = 线程等待资源
- 等待厨师炒菜 = I/O阻塞
- 等待使用公共餐具 = 锁阻塞
- 等朋友一起吃 = 线程间等待
2. 具体场景演示
2.1 资源阻塞(像餐厅座位不够)
public class RestaurantDemo {
// 餐厅总共有5个座位
static final int TOTAL_SEATS = 5;
static int availableSeats = TOTAL_SEATS;
public static void main(String[] args) {
// 模拟10个顾客来餐厅
for (int i = 1; i <= 10; i++) {
final int customerNo = i;
new Thread(() -> {
try {
System.out.println("顾客" + customerNo + "来到餐厅");
// 尝试占座
synchronized (RestaurantDemo.class) {
if (availableSeats > 0) {
availableSeats--;
System.out.println("顾客" + customerNo + "成功占到座位,剩余座位:" + availableSeats);
// 模拟就餐时间
Thread.sleep(1000);
// 释放座位
availableSeats++;
System.out.println("顾客" + customerNo + "离开餐厅,剩余座位:" + availableSeats);
} else {
System.out.println("顾客" + customerNo + "没有座位,等待中...");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Customer-" + i).start();
}
}
}
2.2 I/O阻塞(像等待厨师炒菜)
public class KitchenDemo {
public static void main(String[] args) {
// 模拟点餐队列
BlockingQueue<String> orderQueue = new LinkedBlockingQueue<>();
// 厨师线程
Thread chef = new Thread(() -> {
while (true) {
try {
// 等待订单(阻塞)
String order = orderQueue.take();
System.out.println("厨师开始制作:" + order);
// 模拟炒菜时间
Thread.sleep(2000);
System.out.println(order + " 制作完成!");
} catch (InterruptedException e) {
break;
}
}
}, "Chef");
// 服务员线程
Thread waiter = new Thread(() -> {
String[] dishes = {"宫保鸡丁", "鱼香肉丝", "麻婆豆腐"};
for (String dish : dishes) {
try {
System.out.println("服务员收到订单:" + dish);
orderQueue.put(dish);
// 模拟接单间隔
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}, "Waiter");
chef.start();
waiter.start();
}
}
2.3 锁阻塞(像共用餐具)
public class DiningDemo {
static class Chopstick {
private final String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子" + name;
}
}
public static void main(String[] args) {
// 创建两双筷子
Chopstick chopstick1 = new Chopstick("1");
Chopstick chopstick2 = new Chopstick("2");
// 两个人吃饭
Thread person1 = new Thread(() -> {
while (true) {
synchronized (chopstick1) {
System.out.println("小明拿起了" + chopstick1);
synchronized (chopstick2) {
System.out.println("小明拿起了" + chopstick2);
System.out.println("小明开始吃饭");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "小明");
Thread person2 = new Thread(() -> {
while (true) {
synchronized (chopstick2) {
System.out.println("小红拿起了" + chopstick2);
synchronized (chopstick1) {
System.out.println("小红拿起了" + chopstick1);
System.out.println("小红开始吃饭");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "小红");
person1.start();
person2.start();
}
}
2.4 线程等待(像等朋友一起吃饭)
public class FriendsDiningDemo {
public static void main(String[] args) {
Thread friend1 = new Thread(() -> {
System.out.println("小明到达餐厅");
try {
// 模拟点菜时间
Thread.sleep(2000);
System.out.println("小明点好了菜");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "小明");
Thread friend2 = new Thread(() -> {
try {
System.out.println("小红在等小明点菜");
// 等待friend1完成
friend1.join();
System.out.println("小红看到小明点好菜,开始吃饭");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "小红");
friend2.start();
friend1.start();
}
}
3. 线程状态流转图(用餐厅场景解释)
graph TD
A[新来的顾客] -->|进入餐厅| B[等待入座的顾客]
B -->|有空位| C[正在就餐]
C -->|等待服务员| D[等待服务]
C -->|等待餐具| E[等待资源]
D -->|服务完成| C
E -->|获得餐具| C
C -->|用餐完毕| F[离开餐厅]
4. 如何避免不必要的等待
- 预定座位(线程池)
// 餐厅预定系统
ExecutorService restaurant = Executors.newFixedThreadPool(5);
restaurant.submit(() -> {
System.out.println("顾客就餐");
});
- 叫号系统(非阻塞等待)
public class NumberSystem {
private ConcurrentLinkedQueue<String> waitingList = new ConcurrentLinkedQueue<>();
public void addCustomer(String customer) {
waitingList.offer(customer);
}
public String callNext() {
return waitingList.poll();
}
}
- 自助餐(避免锁竞争)
// 使用并发集合代替同步集合
ConcurrentHashMap<String, Integer> dishes = new ConcurrentHashMap<>();
dishes.put("炒饭", 10); // 无需加锁
5. 实际应用建议
-
合理安排资源
- 就像餐厅要准备足够的桌椅
- 系统要分配合适的内存和线程数
-
避免长时间占用
- 就像就餐时间不要太长
- 程序中不要长时间持有锁
-
设置超时机制
- 就像餐厅设置最长用餐时间
- 程序设置等待超时时间
-
使用排队系统
- 就像餐厅使用叫号系统
- 程序使用队列管理任务
这样的类比希望能帮助你更好地理解线程阻塞的概念!
6. run()与start()的区别
- run(): 普通方法调用,在当前线程执行
- start(): 启动新线程,由JVM调用run()方法
让我用生动的例子来解释连续调用线程的 start() 两次会发生什么。
1. 简单示例
public class DoubleStartDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("线程任务执行...");
}, "MyThread");
try {
thread.start(); // 第一次调用start()
System.out.println("第一次启动线程");
thread.start(); // 第二次调用start()
System.out.println("第二次启动线程"); // 这行代码可能执行不到
} catch (IllegalThreadStateException e) {
System.out.println("发生异常:" + e.getMessage());
e.printStackTrace();
}
}
}
运行结果:
第一次启动线程
线程任务执行...
发生异常:Thread state is RUNNABLE
java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at DoubleStartDemo.main(DoubleStartDemo.java:11)
2. 形象的类比
这就像:
- 🚗 启动一辆已经发动的汽车
- 🏃♂️ 让一个已经在跑步的人再次起跑
- 🎬 重复播放已经在播放的电影
3. 线程状态流转图
stateDiagram-v2
[*] --> NEW: 创建线程
NEW --> RUNNABLE: 第一次start()
RUNNABLE --> TERMINATED: 运行结束
RUNNABLE --> ERROR: 第二次start()抛出异常
4. 更详细的示例
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
// 创建一个线程监视器
Thread monitor = new Thread(() -> {
while (true) {
Thread[] threads = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
Thread.currentThread().getThreadGroup().enumerate(threads);
for (Thread t : threads) {
if (t != null && t.getName().equals("WorkerThread")) {
System.out.println("工作线程状态: " + t.getState());
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}, "MonitorThread");
monitor.setDaemon(true);
monitor.start();
// 创建工作线程
Thread worker = new Thread(() -> {
System.out.println("工作线程开始执行...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "WorkerThread");
// 第一次启动
System.out.println("=== 第一次调用start() ===");
worker.start();
Thread.sleep(500);
try {
// 第二次启动
System.out.println("\n=== 第二次调用start() ===");
worker.start();
} catch (IllegalThreadStateException e) {
System.out.println("无法重复启动线程!");
System.out.println("异常信息:" + e.getMessage());
}
}
}
5. 为什么会抛出异常?
// Thread.java 源码分析(简化版)
public synchronized void start() {
// 检查线程状态
if (threadStatus != 0) {
throw new IllegalThreadStateException();
}
// ... 其他启动逻辑
}
6. 正确的做法
如果需要重复执行任务,有以下几种方案:
- 创建新线程
public class CorrectWay1 {
public static void main(String[] args) {
Runnable task = () -> System.out.println("任务执行...");
// 第一次执行
new Thread(task).start();
// 第二次执行
new Thread(task).start();
}
}
- 使用线程池
public class CorrectWay2 {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
Runnable task = () -> System.out.println("任务执行...");
// 多次提交任务
executor.submit(task);
executor.submit(task);
executor.shutdown();
}
}
- 循环执行
public class CorrectWay3 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("任务执行...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
thread.start();
}
}
7. 最佳实践建议
graph TD
A[需要执行多次任务?] --> B{是否并发执行?}
B -->|是| C[使用线程池]
B -->|否| D[循环执行]
C --> E[ExecutorService]
D --> F[while循环]
- 使用线程池管理线程
public class BestPractice {
private static final ExecutorService executor =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public static void executeTask(Runnable task) {
executor.submit(task);
}
public static void shutdown() {
executor.shutdown();
}
}
- 任务封装
public class TaskWrapper implements Runnable {
private final Runnable task;
public TaskWrapper(Runnable task) {
this.task = task;
}
@Override
public void run() {
try {
task.run();
} catch (Exception e) {
// 处理异常
}
}
}
记住:
- 线程只能启动一次
- 需要重复执行任务时使用线程池
- 合理管理线程资源
- 注意异常处理
这样可以避免重复启动线程的问题,同时提高程序的性能和可维护性。
7. synchronized和volatile的区别
-
synchronized:
- 保证原子性、可见性、有序性
- 可以锁定方法或代码块
- 性能开销较大
-
volatile:
- 只保证可见性和有序性
- 不保证原子性
- 性能好,仅修改内存可见性
8. 保证线程安全的方法
让我用生动的例子来说明保证线程安全的各种方法。
1. 线程安全的方法总览
mindmap
root((线程安全方法))
同步锁
synchronized
Lock接口
ReentrantLock
并发容器
ConcurrentHashMap
CopyOnWriteArrayList
BlockingQueue
原子类
AtomicInteger
AtomicReference
LongAdder
线程本地存储
ThreadLocal
不可变对象
final修饰
String
Integer
2. 具体方法详解
让我用生动的比喻来解释每种线程安全方法的原理。
1. synchronized(同步锁)
🚽 生活场景:公共卫生间
graph TD
A[人来上厕所] --> B{门是否被锁?}
B -->|是| C[等待]
B -->|否| D[进入并锁门]
C --> B
D --> E[使用完毕]
E --> F[开门离开]
public class Bathroom {
public synchronized void use(String person) {
System.out.println(person + "锁门进入");
try {
Thread.sleep(1000); // 使用中
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(person + "使用完毕,开门离开");
}
}
原理解释:
- 就像卫生间只能一个人用
- synchronized 就是那个门锁
- 其他人必须等当前使用者出来才能进去
2. Lock(可重入锁)
🏦 生活场景:银行保险箱
graph TD
A[客户] --> B{尝试开锁}
B -->|成功| C[获得保险箱使用权]
B -->|失败| D[等待或离开]
C --> E[使用保险箱]
E --> F[主动上锁]
public class SafeBox {
private final Lock lock = new ReentrantLock();
public void accessBox(String person) {
if (lock.tryLock()) {
try {
System.out.println(person + "成功打开保险箱");
// 使用保险箱
} finally {
lock.unlock(); // 一定要记得上锁
System.out.println(person + "关闭保险箱");
}
} else {
System.out.println(person + "无法访问保险箱,已被占用");
}
}
}
原理解释:
- 比synchronized更灵活
- 可以尝试获取锁(tryLock)
- 可以设置等待时间
- 必须手动解锁(像保险箱必须手动关门)
3. 并发容器
🛒 生活场景:超市购物车系统
graph LR
A[商品架] --> B{ConcurrentHashMap}
B --> C[顾客1购物车]
B --> D[顾客2购物车]
B --> E[顾客3购物车]
public class SuperMarket {
// 商品库存管理
private final ConcurrentHashMap<String, Integer> inventory = new ConcurrentHashMap<>();
// 结账队列
private final BlockingQueue<String> checkoutQueue = new LinkedBlockingQueue<>();
public void addToCart(String customer, String item) {
// 原子性操作,确保库存准确
inventory.computeIfPresent(item, (k, stock) -> {
if (stock > 0) {
checkoutQueue.offer(customer);
return stock - 1;
}
return stock;
});
}
}
原理解释:
- 像分区收银台,多个收银员同时工作
- 内部分段锁,减少竞争
- 写时复制机制,读操作不受影响
4. 原子类
💰 生活场景:银行账户余额
graph TD
A[账户余额] --> B{CAS操作}
B -->|成功| C[更新余额]
B -->|失败| D[重试]
D --> B
public class BankAccount {
private final AtomicInteger balance;
public boolean withdraw(int amount) {
while (true) {
int current = balance.get();
if (current < amount) {
return false; // 余额不足
}
if (balance.compareAndSet(current, current - amount)) {
return true; // 扣款成功
}
// 如果失败,自动重试
}
}
}
原理解释:
- 像银行柜员在数钱
- 看到余额是100,要取50
- 再次确认还是100才进行操作
- 如果金额已变,重新确认
让我通过示例说明原子类在线程中使用的优缺点:
class AtomicExample {
// 1. 基本原子类使用
private val atomicInt = AtomicInteger(0)
private var normalInt = 0
fun compareCounter() {
// 创建100个线程
repeat(100) {
thread {
// 原子操作
atomicInt.incrementAndGet()
// 非原子操作
normalInt++
}
}
Thread.sleep(1000)
println("Atomic结果: ${atomicInt.get()}") // 总是100
println("Normal结果: $normalInt") // 可能小于100
}
// 2. CAS操作示例
private val atomicRef = AtomicReference<String>("初始值")
fun updateValue(newValue: String): Boolean {
val oldValue = atomicRef.get()
return atomicRef.compareAndSet(oldValue, newValue)
}
}
优点:
class AtomicAdvantages {
// 1. 无锁操作,性能好
private val counter = AtomicInteger(0)
fun increment() {
counter.incrementAndGet() // 无需加锁
}
// 2. 避免死锁
private val state = AtomicReference<String>("INIT")
fun updateState(newState: String) {
// 不会发生死锁
while (!state.compareAndSet(state.get(), newState)) {
Thread.sleep(1)
}
}
// 3. 原子操作保证
private val balance = AtomicInteger(1000)
fun withdraw(amount: Int): Boolean {
while (true) {
val current = balance.get()
if (current < amount) return false
if (balance.compareAndSet(current, current - amount)) {
return true
}
}
}
}
缺点:
class AtomicDisadvantages {
// 1. 只能保证单个操作原子性
private val x = AtomicInteger(0)
private val y = AtomicInteger(0)
fun updateBoth() {
// 不能保证x和y的同时原子性
x.incrementAndGet()
y.incrementAndGet()
}
// 2. 忙等待可能浪费CPU
private val value = AtomicReference<String>(null)
fun busyWaiting() {
while (value.get() == null) {
// 不断循环检查,消耗CPU
Thread.sleep(100)
}
}
// 3. 复杂操作难以实现
private val users = AtomicReference(listOf<User>())
fun addUser(user: User) {
while (true) {
val current = users.get()
val new = current + user
if (users.compareAndSet(current, new)) break
// 如果并发修改频繁,可能需要多次尝试
}
}
}
总结:
优点:
- 无锁操作,性能好
- 避免死锁风险
- 适合简单的原子操作
- API简单易用
- 不需要显式加锁解锁
缺点:
- 只能保证单个操作原子性
- CAS操作可能导致CPU忙等待
- 复杂操作实现困难
- 多个原子变量无法保证一致性
- 可能出现ABA问题
使用建议:
class AtomicUsageTips {
// 1. 简单计数器使用原子类
private val counter = AtomicInteger(0)
// 2. 复杂操作使用锁
private val lock = ReentrantLock()
private var complexState = ComplexObject()
// 3. 状态标志使用原子类
private val status = AtomicReference<Status>(Status.INIT)
// 4. 配合其他同步工具使用
private val ready = AtomicBoolean(false)
private val condition = Condition()
}
根据实际场景选择:
- 简单计数用原子类
- 复杂操作用锁
- 需要事务性操作用锁
- 状态标志用原子类
- 考虑性能和复杂度权衡
5. ThreadLocal
👜 生活场景:私人储物柜
graph TD
A[储物柜系统] --> B[顾客1的柜子]
A --> C[顾客2的柜子]
A --> D[顾客3的柜子]
public class PersonalLocker {
private static final ThreadLocal<List<String>> items =
ThreadLocal.withInitial(ArrayList::new);
public void addItem(String item) {
items.get().add(item); // 每个线程操作自己的物品列表
}
public List<String> getItems() {
return items.get(); // 获取当前线程的物品
}
}
原理解释:
- 每个线程像有自己的私人储物柜
- 互不干扰,完全独立
- 用完记得清理(remove),否则可能内存泄漏
6. 不可变对象
🏺 生活场景:博物馆展品
graph LR
A[创建展品] --> B[展品信息固定]
B --> C[游客1查看]
B --> D[游客2查看]
B --> E[游客3查看]
public final class Artifact {
private final String name;
private final String description;
private final LocalDate discoveryDate;
public Artifact(String name, String description, LocalDate discoveryDate) {
this.name = name;
this.description = description;
this.discoveryDate = discoveryDate;
}
// 只提供getter方法
public String getName() { return name; }
}
原理解释:
- 像博物馆的展品,一旦创建就不能修改
- 所有属性都是final
- 没有setter方法
- 多人同时看也没问题
7. 综合应用示例:餐厅点餐系统
public class Restaurant {
// 菜品库存
private final ConcurrentHashMap<String, AtomicInteger> inventory;
// 订单队列
private final BlockingQueue<Order> orderQueue;
// 每个服务员的订单统计
private final ThreadLocal<Integer> orderCount;
// 厨房访问锁
private final Lock kitchenLock;
static class Order {
private final String dishName; // 不可变
private final int tableNumber; // 不可变
private volatile boolean isReady; // 状态可变
public Order(String dishName, int tableNumber) {
this.dishName = dishName;
this.tableNumber = tableNumber;
this.isReady = false;
}
}
public void placeOrder(Order order) {
// 检查库存
AtomicInteger stock = inventory.get(order.dishName);
if (stock != null && stock.decrementAndGet() >= 0) {
// 放入订单队列
orderQueue.offer(order);
// 更新服务员统计
orderCount.set(orderCount.get() + 1);
}
}
public void cookOrder() {
kitchenLock.lock();
try {
Order order = orderQueue.poll();
if (order != null) {
// 烹饪过程
order.isReady = true;
}
} finally {
kitchenLock.unlock();
}
}
}
这个系统展示了如何组合使用各种线程安全机制:
- ConcurrentHashMap 管理库存
- BlockingQueue 处理订单队列
- ThreadLocal 统计服务员业绩
- Lock 控制厨房访问
- 不可变对象存储订单信息
- volatile 标记订单状态
记住:选择合适的线程安全机制就像选择合适的工具,要根据具体场景来决定!
原子类和ABA问题
详细解释 CAS(Compare And Swap)和 ABA 问题:
- CAS 的生活类比: 想象你去图书馆借书:
// 场景:借书过程
class Library {
// 书的状态(在架上 = true,被借走 = false)
private var bookAvailable = true
fun borrowBook(): Boolean {
// 你看到书在架上(true),准备去拿
val expectedState = true
// 你希望把状态改成被借走(false)
val newState = false
// 但在你拿书的过程中,可能别人已经借走了
return if (bookAvailable == expectedState) {
// 如果书还在,就成功借到
bookAvailable = newState
true
} else {
// 书已经被借走了
false
}
}
}
- 实际的 CAS 操作:
class AtomicCounter {
private val atomic = AtomicInteger(0)
fun increment() {
while (true) {
// 1. 读取当前值
val current = atomic.get()
// 2. 计算新值
val next = current + 1
// 3. 尝试更新,如果成功就退出,失败就重试
if (atomic.compareAndSet(current, next)) {
break
}
// 如果失败,说明其他线程修改了值,重新尝试
}
}
}
- ABA 问题的生动例子: 想象你在食堂排队打饭:
class Canteen {
// 你前面的人
private val personAhead = AtomicReference<Person>()
fun checkQueue() {
// 1. 你看到前面是张三
val zhangsan = personAhead.get()
// 2. 你低头看手机
Thread.sleep(1000)
// 3. 再抬头时,还是张三
// 但实际上:
// - 张三已经打完饭走了
// - 李四来排队了
// - 李四打完饭走了
// - 张三又回来重新排队了
// 4. 你以为队伍没变,但实际上经历了 A -> B -> A 的变化
if (personAhead.compareAndSet(zhangsan, null)) {
println("以为队伍没变,但实际上发生了变化")
}
}
}
- 解决 ABA 问题: 使用版本号:
class VersionedReference<T> {
private val atomic = AtomicStampedReference<T>(null, 0)
fun update(expected: T, new: T): Boolean {
// 获取当前值和版本号
val (currentValue, stamp) = atomic.get()
// 更新时要求值和版本号都匹配
return atomic.compareAndSet(expected, new, stamp, stamp + 1)
}
}
// 使用示例
class BankAccount {
private val balance = VersionedReference<Int>()
fun transfer(amount: Int): Boolean {
while (true) {
// 获取当前余额和版本号
val (currentBalance, stamp) = balance.get()
val newBalance = currentBalance - amount
// 确保余额和版本号都没变才能转账
if (balance.compareAndSet(
currentBalance,
newBalance,
stamp,
stamp + 1
)) {
return true
}
}
}
}
- 实际应用场景:
class ConcurrentStack<T> {
private val top = AtomicReference<Node<T>>(null)
// 入栈操作
fun push(value: T) {
while (true) {
// 获取当前栈顶
val currentTop = top.get()
// 创建新节点
val newNode = Node(value, currentTop)
// 尝试更新栈顶
if (top.compareAndSet(currentTop, newNode)) {
break
}
// 如果失败就重试
}
}
// 出栈操作
fun pop(): T? {
while (true) {
val currentTop = top.get() ?: return null
// 尝试将栈顶更新为下一个节点
if (top.compareAndSet(currentTop, currentTop.next)) {
return currentTop.value
}
}
}
private class Node<T>(
val value: T,
val next: Node<T>?
)
}
总结:
-
CAS 的核心思想:
- "看到什么" - 期望值
- "想改成什么" - 新值
- "确认没人改过" - 比较
- "才动手改" - 交换
-
ABA 问题:
- 表面看值没变(还是A)
- 实际已经发生了变化(A -> B -> A)
- 可能导致程序错误
-
解决方案:
- 使用版本号
- AtomicStampedReference
- AtomicMarkableReference
-
使用建议:
- 考虑是否需要关心中间状态
- 合理使用版本号机制
- 注意性能开销
- 正确处理并发场景
9. ThreadLocal用法和原理
ThreadLocal为每个线程提供独立的变量副本:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("当前线程的值");
String value = threadLocal.get();
原理:
- 每个Thread维护一个ThreadLocalMap
- ThreadLocalMap存储ThreadLocal和对应的值
- 每个线程访问自己的Map,实现线程隔离
让我用生动的例子来解释 ThreadLocal 的用法和原理。
1. ThreadLocal 形象比喻:员工储物柜系统 🏢
想象一个公司的储物柜系统:
- 每个员工都有自己的私人储物柜
- 储物柜编号相同,但属于不同员工
- 员工只能访问自己的储物柜
2. 基本用法示例
public class EmployeeLocker {
// 创建储物柜系统
private static final ThreadLocal<String> personalLocker = new ThreadLocal<>();
// 创建带初始值的储物柜系统
private static final ThreadLocal<List<String>> itemLocker =
ThreadLocal.withInitial(ArrayList::new);
public static void main(String[] args) {
// 创建多个员工(线程)
Thread employee1 = new Thread(() -> {
personalLocker.set("员工1的物品");
itemLocker.get().add("钱包");
itemLocker.get().add("手机");
System.out.println("员工1的储物柜: " + personalLocker.get());
System.out.println("员工1的物品清单: " + itemLocker.get());
// 很重要:用完要清理!
personalLocker.remove();
itemLocker.remove();
}, "员工1");
Thread employee2 = new Thread(() -> {
personalLocker.set("员工2的物品");
itemLocker.get().add("钥匙");
itemLocker.get().add("水杯");
System.out.println("员工2的储物柜: " + personalLocker.get());
System.out.println("员工2的物品清单: " + itemLocker.get());
// 很重要:用完要清理!
personalLocker.remove();
itemLocker.remove();
}, "员工2");
employee1.start();
employee2.start();
}
}
3. ThreadLocal 原理流程图
graph TD
A[Thread对象] --> B[ThreadLocalMap]
B --> C[Entry数组]
C --> D1[Entry1: ThreadLocal1->值1]
C --> D2[Entry2: ThreadLocal2->值2]
C --> D3[Entry3: ThreadLocal3->值3]
E[ThreadLocal对象] -.引用.-> D1
E -.获取当前线程.-> A
4. 完整的工作流程示例
public class ThreadLocalWorkflow {
// 用户上下文
private static class UserContext {
private String userId;
private String userName;
private List<String> permissions;
public UserContext(String userId, String userName) {
this.userId = userId;
this.userName = userName;
this.permissions = new ArrayList<>();
}
}
// 创建ThreadLocal存储用户上下文
private static final ThreadLocal<UserContext> userContextHolder = new ThreadLocal<>();
// 模拟处理用户请求的服务
static class UserService {
public void processUserRequest(String userId, String userName) {
try {
// 设置用户上下文
UserContext context = new UserContext(userId, userName);
userContextHolder.set(context);
// 模拟业务处理
System.out.println(Thread.currentThread().getName() +
" 处理用户请求: " + userName);
// 调用其他服务
new AuthService().checkPermissions();
new DataService().getData();
} finally {
// 清理上下文
userContextHolder.remove();
}
}
}
// 模拟权限检查服务
static class AuthService {
public void checkPermissions() {
UserContext context = userContextHolder.get();
System.out.println(Thread.currentThread().getName() +
" 检查权限: " + context.userName);
}
}
// 模拟数据访问服务
static class DataService {
public void getData() {
UserContext context = userContextHolder.get();
System.out.println(Thread.currentThread().getName() +
" 获取数据: " + context.userName);
}
}
public static void main(String[] args) {
UserService userService = new UserService();
// 模拟多个用户请求
for (int i = 1; i <= 3; i++) {
final String userId = "user" + i;
final String userName = "User " + i;
new Thread(() -> {
userService.processUserRequest(userId, userName);
}, "Thread-" + i).start();
}
}
}
5. 详细的工作原理图
sequenceDiagram
participant Thread as 线程
participant TL as ThreadLocal对象
participant TLMap as ThreadLocalMap
participant Entry as Entry数组
Thread->>TL: 调用set(value)
TL->>Thread: 获取当前线程
Thread->>TLMap: 获取ThreadLocalMap
TL->>TLMap: 设置键值对
TLMap->>Entry: 存储Entry(ThreadLocal, value)
Note over Thread,Entry: 获取值的流程
Thread->>TL: 调用get()
TL->>Thread: 获取当前线程
Thread->>TLMap: 获取ThreadLocalMap
TLMap->>Entry: 查找对应Entry
Entry-->>Thread: 返回值
6. 常见使用场景
- 用户上下文传递
public class UserContextHolder {
private static final ThreadLocal<User> userContext = new ThreadLocal<>();
public static void setUser(User user) {
userContext.set(user);
}
public static User getUser() {
return userContext.get();
}
public static void clear() {
userContext.remove();
}
}
- 数据库连接管理
public class ConnectionManager {
private static final ThreadLocal<Connection> connectionHolder =
ThreadLocal.withInitial(() -> {
try {
return DriverManager.getConnection("jdbc:mysql://localhost/db");
} catch (SQLException e) {
throw new RuntimeException(e);
}
});
public static Connection getConnection() {
return connectionHolder.get();
}
}
- 请求追踪
public class RequestTracer {
private static final ThreadLocal<String> requestId =
ThreadLocal.withInitial(() -> UUID.randomUUID().toString());
public static void trace(String message) {
System.out.println(requestId.get() + ": " + message);
}
}
7. 注意事项
graph TD
A[ThreadLocal使用注意事项] --> B[内存泄漏风险]
A --> C[线程池使用注意]
A --> D[初始值设置]
B --> B1[及时调用remove]
B --> B2[使用try-finally]
C --> C1[线程复用问题]
C --> C2[清理上下文]
D --> D1[使用withInitial]
D --> D2[避免空指针]
8. 最佳实践
public class ThreadLocalBestPractice {
private static final ThreadLocal<Resource> resourceHolder =
ThreadLocal.withInitial(Resource::new);
public void processRequest() {
try {
// 使用ThreadLocal中的资源
Resource resource = resourceHolder.get();
resource.process();
} finally {
// 请求结束后清理
resourceHolder.remove();
}
}
static class Resource {
public void process() {
// 处理逻辑
}
}
}
记住:
- ThreadLocal 像私人储物柜,每个线程都有自己的空间
- 用完一定要记得清理(remove),否则可能造成内存泄漏
- 在线程池环境下要特别注意清理工作
- 适合存储线程私有的数据,不适合线程间共享数据
9.流程图总结
让我用流程图来总结 ThreadLocal、ThreadLocalMap 和 Thread 的关系:
graph TD
A[Thread-1] --> B[ThreadLocalMap-1]
A1[Thread-2] --> B1[ThreadLocalMap-2]
C[ThreadLocal] --> D{获取当前线程}
D -->|Thread-1| E[获取ThreadLocalMap-1]
D -->|Thread-2| E1[获取ThreadLocalMap-2]
E --> F[存储/获取值]
E1 --> F1[存储/获取值]
F --> G[Entry数组]
F1 --> G1[Entry数组]
G --> H[Entry-1<br>key=ThreadLocal<br>value=值1]
G --> I[Entry-2<br>key=ThreadLocal<br>value=值2]
G1 --> H1[Entry-1<br>key=ThreadLocal<br>value=值3]
G1 --> I1[Entry-2<br>key=ThreadLocal<br>value=值4]
操作流程:
sequenceDiagram
participant T as Thread
participant TL as ThreadLocal
participant TLM as ThreadLocalMap
T->>TL: 调用set/get
TL->>T: 获取当前线程
T->>TLM: 获取ThreadLocalMap
TLM->>TLM: 根据ThreadLocal存取值
TLM-->>T: 返回结果
数据结构:
classDiagram
class Thread {
-ThreadLocalMap threadLocals
+getThreadLocals()
}
class ThreadLocalMap {
-Entry[] table
+set(ThreadLocal key, Object value)
+get(ThreadLocal key)
}
class ThreadLocal {
+set(T value)
+get()
+remove()
}
Thread "1" --> "1" ThreadLocalMap
ThreadLocalMap "1" --> "*" Entry
Entry "*" --> "1" ThreadLocal
这样的关系就更清晰了:
- 每个Thread有一个ThreadLocalMap
- ThreadLocal作为key存取值
- 不同线程的值互不干扰
10. notify和notifyAll的区别
- notify(): 随机唤醒一个等待的线程
- notifyAll(): 唤醒所有等待的线程
11. 线程池及创建
线程池的好处:
- 重用线程,减少创建销毁开销
- 控制并发数
- 管理线程
创建方式:
// 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 缓存线程池
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 单线程池
ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 调度线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(5);
让我用一个餐厅服务员管理系统来形象地解释线程池的原理和应用。
1. 线程池比喻:餐厅服务员管理系统 🏪
graph TD
A[餐厅管理系统] --> B[核心服务员<br>corePoolSize]
A --> C[临时服务员<br>maximumPoolSize]
A --> D[任务队列<br>workQueue]
A --> E[顾客等待策略<br>RejectedExecutionHandler]
B --> F[固定班服务员]
C --> G[临时工]
D --> H[等待区]
E --> I[拒绝服务]
2. 线程池的创建方式
public class RestaurantThreadPool {
public static void main(String[] args) {
// 1. 固定大小的餐厅 (像快餐店)
ExecutorService fixedRestaurant = Executors.newFixedThreadPool(5);
// 2. 弹性伸缩的餐厅 (像火锅店)
ExecutorService cachedRestaurant = Executors.newCachedThreadPool();
// 3. 单一服务员的餐厅 (像小摊)
ExecutorService singleWaiter = Executors.newSingleThreadExecutor();
// 4. 定时营业的餐厅 (像早餐店)
ScheduledExecutorService scheduledRestaurant =
Executors.newScheduledThreadPool(3);
// 5. 自定义餐厅
ThreadPoolExecutor customRestaurant = new ThreadPoolExecutor(
3, // 核心服务员数量
5, // 最大服务员数量
60L, // 临时工最大空闲时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(100), // 等待区大小
new ThreadFactory() { // 服务员招聘工厂
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("服务员-" + t.getId());
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 客满策略
);
}
}
3. 实际应用示例:餐厅服务系统
public class RestaurantService {
// 创建餐厅服务池
private final ThreadPoolExecutor restaurantPool;
// 订单队列
private final BlockingQueue<Order> orderQueue;
static class Order {
String customerName;
String dishName;
public Order(String customerName, String dishName) {
this.customerName = customerName;
this.dishName = dishName;
}
}
public RestaurantService() {
// 初始化线程池
this.restaurantPool = new ThreadPoolExecutor(
3, // 核心服务员
5, // 最大服务员
60L, TimeUnit.SECONDS, // 临时工最大空闲时间
new LinkedBlockingQueue<>(100), // 等待区大小
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("服务员-" + t.getId());
return t;
}
},
// 客满后,让客人稍后再来
new ThreadPoolExecutor.CallerRunsPolicy()
);
this.orderQueue = new LinkedBlockingQueue<>();
}
// 处理订单
public void processOrder(Order order) {
restaurantPool.execute(() -> {
try {
System.out.println(Thread.currentThread().getName() +
" 开始处理 " + order.customerName + " 的 " +
order.dishName + " 订单");
// 模拟订单处理时间
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() +
" 完成了 " + order.customerName + " 的订单");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭餐厅
public void closeRestaurant() {
restaurantPool.shutdown();
}
public static void main(String[] args) {
RestaurantService restaurant = new RestaurantService();
// 模拟多个顾客点餐
for (int i = 1; i <= 10; i++) {
final String customerName = "顾客" + i;
final String dishName = "菜品" + i;
restaurant.processOrder(new Order(customerName, dishName));
}
// 等待所有订单处理完毕后关闭餐厅
restaurant.closeRestaurant();
}
}
4. 线程池工作流程图
sequenceDiagram
participant Customer as 顾客
participant Pool as 线程池
participant CoreWorker as 核心服务员
participant Queue as 等待队列
participant TempWorker as 临时服务员
Customer->>Pool: 提交任务
alt 核心服务员有空
Pool->>CoreWorker: 分配任务
else 核心服务员都忙
alt 等待队列未满
Pool->>Queue: 加入等待队列
else 等待队列已满
alt 未达到最大服务员数
Pool->>TempWorker: 招临时工处理
else 已达到最大服务员数
Pool->>Customer: 执行拒绝策略
end
end
end
5. 线程池监控示例
public class RestaurantMonitor {
private final ThreadPoolExecutor pool;
public RestaurantMonitor(ThreadPoolExecutor pool) {
this.pool = pool;
}
public void printStats() {
System.out.println("=========================");
System.out.println("餐厅服务状态:");
System.out.println("当前服务员数量: " + pool.getPoolSize());
System.out.println("核心服务员数量: " + pool.getCorePoolSize());
System.out.println("正在工作的服务员: " + pool.getActiveCount());
System.out.println("等待队列长度: " + pool.getQueue().size());
System.out.println("已完成的订单: " + pool.getCompletedTaskCount());
System.out.println("=========================");
}
}
6. 不同场景的线程池配置
让我详细解释不同场景下线程池的配置参数,用餐厅类型来比喻。
1. 快餐店模式(FixedThreadPool)
public static ThreadPoolExecutor fastFoodRestaurant() {
return new ThreadPoolExecutor(
5, // 核心线程数(固定服务员)
5, // 最大线程数(与核心数相同,不会雇佣临时工)
0L, // 空闲线程存活时间(专职服务员,不会被解雇)
TimeUnit.MILLISECONDS, // 时间单位
new LinkedBlockingQueue<>(100) // 有限等待队列(最多排队100个顾客)
);
}
特点说明:
graph TD
A[快餐店特点] --> B[固定服务员数量]
A --> C[持续工作不解雇]
A --> D[有限等待队列]
B --> B1[适合负载稳定]
C --> C1[保证服务质量]
D --> D1[防止顾客过多]
2. 火锅店模式(CachedThreadPool)
public static ThreadPoolExecutor hotPotRestaurant() {
return new ThreadPoolExecutor(
2, // 核心线程数(基本服务员)
10, // 最大线程数(高峰期可增加临时工)
60L, // 空闲线程存活时间(临时工最多空闲1分钟)
TimeUnit.SECONDS, // 时间单位
new SynchronousQueue<>() // 直接交付队列(来一个顾客立即安排)
);
}
特点说明:
graph TD
A[火锅店特点] --> B[弹性服务员数量]
A --> C[临时工制度]
A --> D[即时响应]
B --> B1[应对客流波动]
C --> C1[成本优化]
D --> D1[无需排队等待]
3. 高档餐厅模式(PriorityThreadPool)
public static ThreadPoolExecutor fancyRestaurant() {
return new ThreadPoolExecutor(
5, // 核心线程数(专业服务员)
10, // 最大线程数(重要场合增加人手)
60L, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new PriorityBlockingQueue<>(), // 优先级队列(VIP客人优先服务)
new ThreadFactory() { // 自定义服务员创建工厂
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setPriority(Thread.MAX_PRIORITY); // 高优先级服务
return t;
}
}
);
}
特点说明:
graph TD
A[高档餐厅特点] --> B[优质服务团队]
A --> C[灵活扩容]
A --> D[VIP优先]
B --> B1[专业服务保证]
C --> C1[应对特殊场合]
D --> D1[优先级任务处理]
4. 早餐店模式(ScheduledThreadPool)
public static ScheduledThreadPoolExecutor breakfastRestaurant() {
return new ScheduledThreadPoolExecutor(
3, // 核心线程数(固定早班服务员)
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("早班服务员-" + t.getId());
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 客满策略
);
}
使用示例:
ScheduledThreadPoolExecutor breakfast = breakfastRestaurant();
// 定时供应早餐
breakfast.scheduleAtFixedRate(
() -> System.out.println("供应早餐"),
6, // 初始延迟6小时
24, // 每24小时一次
TimeUnit.HOURS
);
// 定时清理餐厅
breakfast.scheduleWithFixedDelay(
() -> System.out.println("清理餐厅"),
8, // 初始延迟8小时
1, // 每1小时一次
TimeUnit.HOURS
);
5. 综合场景配置
public class RestaurantChain {
// IO密集型任务池(如处理订单、支付)
private final ThreadPoolExecutor orderPool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() * 2, // CPU核心数2倍
Runtime.getRuntime().availableProcessors() * 4, // 最大4倍
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("订单处理-" + t.getId());
return t;
}
}
);
// CPU密集型任务池(如计算账单、统计报表)
private final ThreadPoolExecutor computePool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(), // CPU核心数
Runtime.getRuntime().availableProcessors(), // 固定大小
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(100),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("计算任务-" + t.getId());
return t;
}
}
);
}
6. 参数选择建议
graph TD
A[线程池参数选择] --> B[核心线程数]
A --> C[最大线程数]
A --> D[队列类型]
A --> E[拒绝策略]
B --> B1[IO密集型: CPU核心数*2]
B --> B2[计算密集型: CPU核心数]
C --> C1[考虑内存和CPU资源]
C --> C2[避免过度创建线程]
D --> D1[LinkedBlockingQueue: 有限队列]
D --> D2[SynchronousQueue: 直接提交]
D --> D3[PriorityBlockingQueue: 优先级队列]
E --> E1[CallerRunsPolicy: 调用者执行]
E --> E2[AbortPolicy: 抛出异常]
E --> E3[DiscardPolicy: 丢弃任务]
7. 监控指标
public class ThreadPoolMonitor {
private final ThreadPoolExecutor pool;
public void printMetrics() {
System.out.println("=== 线程池状态 ===");
System.out.printf("活跃线程数: %d\n", pool.getActiveCount());
System.out.printf("核心线程数: %d\n", pool.getCorePoolSize());
System.out.printf("最大线程数: %d\n", pool.getMaximumPoolSize());
System.out.printf("线程池大小: %d\n", pool.getPoolSize());
System.out.printf("队列任务数: %d\n", pool.getQueue().size());
System.out.printf("已完成任务: %d\n", pool.getCompletedTaskCount());
System.out.printf("总任务数: %d\n", pool.getTaskCount());
}
}
记住:
- 根据任务类型选择合适的线程池配置
- 考虑系统资源合理设置参数
- 注意监控线程池状态
- 根据实际负载调整参数
- 选择合适的任务队列和拒绝策略
7. 线程池生命周期
stateDiagram-v2
[*] --> RUNNING: 创建线程池
RUNNING --> SHUTDOWN: shutdown()
RUNNING --> STOP: shutdownNow()
SHUTDOWN --> TIDYING: 所有任务完成
STOP --> TIDYING: 所有线程终止
TIDYING --> TERMINATED: terminated()
TERMINATED --> [*]
8. 最佳实践建议
- 合理配置线程池参数
public class ThreadPoolConfig {
public static ThreadPoolExecutor createPool() {
int cpuCores = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
cpuCores, // 核心线程数
cpuCores * 2, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲时间
new LinkedBlockingQueue<>(1000), // 等待队列
new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Worker-" + counter.getAndIncrement());
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
- 任务分类处理
public class TaskProcessor {
private final ThreadPoolExecutor ioPool; // IO密集型任务池
private final ThreadPoolExecutor computePool; // CPU密集型任务池
public void processTask(Runnable task, TaskType type) {
switch (type) {
case IO:
ioPool.execute(task);
break;
case COMPUTE:
computePool.execute(task);
break;
}
}
}
记住:
- 根据任务类型选择合适的线程池
- 合理配置线程池参数
- 注意监控线程池状态
- 优雅关闭线程池
- 处理好任务拒绝策略
Transactions和线程的结合使用
class BankTransfer {
private val connection = getConnection()
// 模拟多个线程同时转账
fun multiThreadTransfer() {
// 创建多个转账线程
val thread1 = thread {
transfer("A", "B", 100) // A转账给B
}
val thread2 = thread {
transfer("B", "C", 200) // B转账给C
}
val thread3 = thread {
transfer("A", "C", 150) // A转账给C
}
// 等待所有转账完成
thread1.join()
thread2.join()
thread3.join()
}
private fun transfer(from: String, to: String, amount: Int) {
val conn = getConnection() // 每个线程独立的连接
conn.autoCommit = false
try {
println("${Thread.currentThread().name} 开始转账: $from -> $to, 金额: $amount")
// 执行转账操作
val stmt = conn.createStatement()
stmt.executeUpdate("UPDATE account SET money = money - $amount WHERE id = '$from'")
stmt.executeUpdate("UPDATE account SET money = money + $amount WHERE id = '$to'")
Thread.sleep(100) // 模拟耗时操作
conn.commit()
println("${Thread.currentThread().name} 转账成功")
} catch (e: Exception) {
conn.rollback()
println("${Thread.currentThread().name} 转账失败: ${e.message}")
} finally {
conn.close()
}
}
}
现在多线程体现在:
- 同时有多个转账操作在执行
- 每个转账都在独立的线程中
- 每个线程都有自己的事务
- 可以看到并发执行的效果
使用示例:
fun main() {
val bank = BankTransfer()
bank.multiThreadTransfer()
}
// 可能的输出:
// Thread-0 开始转账: A -> B, 金额: 100
// Thread-1 开始转账: B -> C, 金额: 200
// Thread-2 开始转账: A -> C, 金额: 150
// Thread-1 转账成功
// Thread-0 转账成功
// Thread-2 转账成功
这样并发效果就很明显了。
线程yield()方法的详解
Thread.yield() 方法的作用是让当前线程让出 CPU 执行权,但这个让出是临时性的,线程状态会发生以下变化:
- 线程状态变化:
public class ThreadYieldDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Before yield: " + Thread.currentThread().getState());
Thread.yield();
System.out.println("After yield: " + Thread.currentThread().getState());
});
thread.start();
}
}
// 输出:
// Before yield: RUNNABLE
// After yield: RUNNABLE
关键点:
- 调用
yield()后,线程从 RUNNABLE 状态变为 READY 状态 - 线程重新等待 CPU 调度
- 线程很快会重新回到 RUNNABLE 状态
- 不会进入 WAITING 或 BLOCKED 状态
- 与其他线程状态的对比:
public class ThreadStatesDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
// yield - 临时让出CPU,仍是RUNNABLE
Thread.yield();
// sleep - 进入TIMED_WAITING状态
Thread.sleep(1000);
// wait - 进入WAITING状态
synchronized(this) {
wait();
}
// park - 进入WAITING状态
LockSupport.park();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
- 实际应用示例:
public class YieldExample {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
System.out.println(
Thread.currentThread().getName() +
" count: " + i
);
Thread.yield(); // 让出CPU
}
};
// 创建两个线程
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
}
}
- yield() 的特点:
class ResourceSharing {
private static int counter = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter++;
if (i % 100 == 0) {
Thread.yield(); // 定期让出CPU
}
}
};
// yield() 不保证公平性
// yield() 不保证其他线程一定能获得执行
// yield() 的效果依赖于操作系统的调度
}
}
- 使用场景:
class CPUIntensiveTask {
public void process() {
while (true) {
// 执行计算密集型任务
doHeavyComputation();
// 定期让出CPU,避免独占CPU
if (shouldYield()) {
Thread.yield();
}
}
}
private boolean shouldYield() {
// 根据具体条件决定是否让出CPU
return System.currentTimeMillis() % 100 == 0;
}
}
总结:
-
yield()的效果:- 线程状态仍然是 RUNNABLE
- 只是临时让出 CPU 执行权
- 很快会被重新调度
-
特点:
- 不保证其他线程一定能执行
- 依赖操作系统的调度策略
- 主要用于调试或优化目的
-
使用建议:
- 不要依赖 yield() 来控制线程执行顺序
- 不要用 yield() 来解决线程同步问题
- 可以用于优化 CPU 密集型任务
- 在调试多线程问题时可以使用
-
替代方案:
- 使用 Thread.sleep() 实现确定的暂停
- 使用 wait()/notify() 实现线程协作
- 使用 Java 并发工具类实现更可靠的线程控制