一、线程安全的风险来源
1.1 后厨的「订单撞单」现象
场景:两服务员同时录入客人点单到同一个菜单本
问题:
- 订单可能被覆盖
- 菜品数量统计错误
Java中的表现:
public class OrderServlet extends HttpServlet {
private int totalOrders = 0; // 共享计数器
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
totalOrders++; // 并发时错误
}
}
二、线程安全三要素
2.1 问题的根源机制
问题类型 | 厨房类比 | 解决方案 | Java对应概念 |
---|---|---|---|
原子性 | 厨师查看库存到下单三步可能中断 | 步骤打包成一个原子操作 | synchronized块 |
可见性 | 更新库存忘记同步到总台 | 中央公告板强制更新状态 | volatile关键字 |
有序性 | 备菜与送餐顺序调换 | 确保工序合理顺序 | 避免指令重排 |
三、核心锁机制synchronized
3.1 后厨传菜窗口的排队系统
public class KitchenService {
private final Object lock = new Object();
public void processOrder(Order order) {
synchronized(lock) { // 类似传菜窗口排队拿号
// 处理食材库存和制作工序
}
}
}
重要特性:
- 重入性:大厨可以多次进出冷藏库(避免自锁)
- 互斥性:同一时间只有一个窗口可取菜
- 作用域:锁定单个对象(传菜窗口)或整个餐馆(类锁)
四、内存屏障volatile原理
4.1 实时更新的库存显示屏
class StockManager {
public volatile int beefStock = 100; // 各后厨实时可见最新库存
public void updateStock(int used) {
beefStock -= used; // 注意:volatile无法保证多步原子性!
}
}
为什么需要 volatile
?
new Singleton()
分为三步:
- 分配内存
- 初始化对象
- 将引用指向内存地址
若没有volatile
,可能 JVM 会将 2 和 3 重排序,导致其他线程拿到未初始化的对象!
解决的问题
.可见性问题(强制刷新内存)
- 问题表现:
A 线程修改了共享变量的值,但 B 线程看到的值还是旧的(因为 CPU 缓存未同步到主存)。 - volatile 作用:
当一个线程修改volatile
变量的值时,其他线程能立即看到最新值,如同直接操作主内存。让所有线程看见最新的自己,且不乱序.
适用场景:
- 菜单状态标记位(如是否停止接单)
- 配置参数实时更新(动态调整菜品价格)
五、生产-消费协作模型
5.1 前厅与后厨的订单协作
public class Restaurant {
private final BlockingQueue<Order> orderQueue = new LinkedBlockingQueue<>(20);
// 服务员提交订单(生产者)
void submitOrder(Order order) throws InterruptedException {
orderQueue.put(order);
}
// 厨房处理订单(消费者)
void processOrders() {
while(true) {
Order order = orderQueue.take();
cook(order);
}
}
}
优势:
- 削峰:用餐高峰避免厨师过载
- 解耦:前厅接单与厨房制作分离
六、单例模式安全实现
6.1 中央调料柜的双重检查
public class SpiceCabinet {
private static volatile SpiceCabinet instance;
public static SpiceCabinet getInstance() {
if (instance == null) { // 第一次快速校验
synchronized(SpiceCabinet.class) {
if (instance == null) { // 二次确认
instance = new SpiceCabinet();
}
}
}
return instance;
}
}
第一个if
语句(外层检查)
if (instance == null) { // 第一次快速校验
-
作用:快速路径 (Fast Path)
- 当实例已经存在时,直接跳过加锁步骤,立即返回实例。
- 当实例不存在时,才会进入同步块竞争锁。
-
解决的问题:减少锁竞争
如果去掉这层检查,每次调用getInstance()
都必须加锁(无论实例是否已存在),导致即便实例已创建,线程仍需排队竞争锁,性能严重下降。
特点:
- 首次访问可能稍慢(需要加锁初始化)
- 后续高效获取(无需锁竞争)
第二个if
语句(内层检查)
if (instance == null) { // 二次确认
-
作用:严格保序 (Strict Serialization)
- 当多个线程同时通过外层检查进入同步块时(初始化阶段),防止多次创建实例。
- 仅第一个获取锁的线程完成初始化,后续线程发现
instance != null
后直接返回。
常见误区说明 ✅❌
错误写法 | 问题根源 | 正确方案 |
---|---|---|
去掉外层if | 每次调用均加锁,性能低下 | 保留外层用于快速路径判断 |
去掉内层if | 允许初始化多次(破坏单例) | 内层确保锁内唯一性 |
无volatile | 可能导致部分初始化对象被访问 | volatile 禁用指令重排和强制同步 |
总结
双检锁双if
的精髓在于:通过两次检查分别应对不同的并发场景
- 外层
if
→ 应对已初始化场景(高性能) - 内层
if
→ 应对未初始化场景(安全性)
同时,volatile
确保了这一机制在极端的并发环境下的最终正确性。
七、线程池管理策略
7.1 后厨人员的弹性调度
ExecutorService kitchenStaff = Executors.newCachedThreadPool();
// 订单到来动态分配厨师
public void handleOrder(Order order) {
kitchenStaff.submit(() -> {
prepareIngredients(order);
cookDish(order);
notifyServing(order);
});
}
资源优化点:
- 核心厨师(corePoolSize)保持随时待命
- 临时工(maximumPoolSize)应对就餐高峰
- 空闲回收(keepAliveTime)降低运营成本
八.故障自排查
现象 | 可能原因 | 检查点 |
---|---|---|
数据不一致 | 未同步共享资源 | 检查++/--操作是否有锁 |
吞吐量下降 | 过度同步或锁竞争 | 使用JConsole观察锁状态 |
CPU利用率异常高 | 忙等待或无限制循环 | 检查是否有sleep/等待机制 |
进程僵死 | 死锁(互相等待资源) | 检测锁获取顺序是否一致 |