Java多线程的应用场景和目的

Java 多线程的目的不是把代码写复杂,而是在合适的场景里提升吞吐、降低等待时间,并让 CPU、网络、磁盘和下游服务资源得到更充分的利用。是否应该使用多线程,关键不在于“会不会开线程”,而在于能否判断任务之间有没有依赖、瓶颈在哪里、并发后是否会破坏数据一致性。

整体流程

Java 多线程业务处理流程

多线程解决的核心问题

多线程常见目标有三类:

  1. 提升吞吐量:让系统在单位时间内处理更多任务。
  2. 降低等待时间:把互不依赖的 IO 操作并行执行,缩短整体响应时间。
  3. 提高资源利用率:当一个线程等待数据库、Redis、HTTP 接口时,其他线程可以继续执行。

但多线程不是默认答案。对于 CPU 密集型任务,线程数超过 CPU 核数太多会带来上下文切换;对于数据库密集型任务,线程过多可能把连接池和数据库压垮;对于共享数据修改,如果没有并发控制,会出现脏数据、重复扣减、状态错乱。

供应链场景:订单详情聚合

订单详情页通常需要展示订单基础信息、库存状态、物流轨迹、应收金额。它们来自不同模块,彼此没有强依赖:

1
2
3
4
订单基础信息 -> OMS
库存状态 -> Inventory Service
物流轨迹 -> TMS
应收金额 -> Finance Service

如果串行执行,每个接口耗时都会累加:

1
订单 30ms + 库存 40ms + 物流 80ms + 财务 50ms = 200ms

如果并行查询,整体耗时更接近最慢的那个任务:

1
max(30ms, 40ms, 80ms, 50ms) + 聚合成本

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public OrderDetail queryOrderDetail(long orderId) {
CompletableFuture<Order> orderFuture =
CompletableFuture.supplyAsync(() -> orderService.get(orderId), bizExecutor);
CompletableFuture<InventoryView> inventoryFuture =
CompletableFuture.supplyAsync(() -> inventoryService.viewByOrder(orderId), bizExecutor);
CompletableFuture<TrackInfo> trackFuture =
CompletableFuture.supplyAsync(() -> trackService.query(orderId), bizExecutor);
CompletableFuture<Receivable> receivableFuture =
CompletableFuture.supplyAsync(() -> financeService.receivable(orderId), bizExecutor);

CompletableFuture.allOf(orderFuture, inventoryFuture, trackFuture, receivableFuture).join();

return new OrderDetail(
orderFuture.join(),
inventoryFuture.join(),
trackFuture.join(),
receivableFuture.join()
);
}

这里的关键点是:这些查询互不依赖,且主要耗时在 IO 等待上,因此适合并行。

不适合并发的情况

下面这种库存扣减逻辑不能简单并行:

1
2
3
4
5
6
7
8
public void reserveStock(String skuCode, int qty) {
int available = inventoryRepository.queryAvailable(skuCode);
if (available < qty) {
throw new BizException("库存不足");
}
inventoryRepository.decreaseAvailable(skuCode, qty);
inventoryRepository.increaseLocked(skuCode, qty);
}

如果多个线程同时读到相同库存,再分别扣减,就可能超卖。库存预占必须依赖数据库行锁、乐观锁、原子更新或 Redis Lua 等机制保护一致性:

1
2
3
4
5
UPDATE scm_inventory
SET available_qty = available_qty - #{qty},
locked_qty = locked_qty + #{qty}
WHERE sku_code = #{skuCode}
AND available_qty >= #{qty};

再通过影响行数判断是否预占成功。

线程池必须受控

生产系统不要为每个请求随意 new Thread()。线程创建和销毁有成本,线程数量失控还会耗尽内存、连接池和下游接口容量。

推荐使用明确的业务线程池:

1
2
3
4
5
6
7
8
9
ThreadPoolExecutor bizExecutor = new ThreadPoolExecutor(
16,
32,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("order-query-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);

线程池配置要结合业务压测结果。队列不能无限大,否则流量高峰时请求会堆积很久,用户看到的是长时间等待而不是快速失败。

判断是否使用多线程的 checklist

使用多线程前可以问几个问题:

  1. 任务之间是否互不依赖。
  2. 瓶颈是 IO 等待还是 CPU 计算。
  3. 并发后是否会修改同一份业务数据。
  4. 线程池、数据库连接池、HTTP 连接池是否有容量上限。
  5. 失败后是否能降级、重试或返回部分结果。
  6. 日志和监控能否定位每个异步任务的耗时和异常。

多线程的收益来自清晰的任务拆分和资源控制。对供应链系统来说,订单聚合查询、采购对账、物流轨迹同步、批量报表生成都可能受益于并发;库存扣减、状态流转、单据审核则必须优先保证一致性。

Java多线程基础:线程生命周期、创建方式和业务边界

Java 多线程基础要掌握三件事:线程处于什么状态、线程任务如何创建、线程之间如何协作。真正写业务代码时,还要判断哪些逻辑适合并发,哪些逻辑必须串行或依赖数据库事务保护。

线程生命周期

Java 线程生命周期

Java 线程常见状态包括:

  1. NEW:线程对象已创建,但还没有调用 start()
  2. RUNNABLE:线程已经可以被 CPU 调度,可能正在运行,也可能等待时间片。
  3. BLOCKED:等待进入 synchronized 临界区。
  4. WAITING:无限期等待其他线程唤醒,例如 Object.wait()Thread.join()
  5. TIMED_WAITING:有超时时间的等待,例如 sleep()、带超时的 wait()
  6. TERMINATED:线程执行结束。

需要注意:调用 start() 后,线程不一定立即执行;调用 run() 只是普通方法调用,不会启动新线程。

创建线程的常见方式

直接继承 Thread

1
2
3
4
5
6
class InventorySyncThread extends Thread {
@Override
public void run() {
syncInventory();
}
}

实现 Runnable

1
2
3
4
5
6
class InventorySyncTask implements Runnable {
@Override
public void run() {
syncInventory();
}
}

使用 Callable 获取返回值:

1
2
3
4
Callable<Integer> task = () -> inventoryService.countLowStockSku();
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();
Integer lowStockCount = futureTask.get();

生产代码中更推荐使用线程池,而不是直接 new Thread()

1
2
ExecutorService executor = Executors.newFixedThreadPool(8);
executor.submit(() -> inventoryService.syncByWarehouse("WH-SH"));

线程池可以控制并发数量、复用线程、设置队列和拒绝策略,更适合服务端应用。

供应链业务例子

库存同步任务可能要同步多个仓库:

1
2
3
4
5
6
7
8
9
10
public void syncAllWarehouses(List<String> warehouseCodes) {
List<CompletableFuture<Void>> futures = warehouseCodes.stream()
.map(code -> CompletableFuture.runAsync(
() -> inventorySyncService.syncWarehouse(code),
inventoryExecutor
))
.toList();

CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}

这个场景适合并发,因为不同仓库之间通常可以独立同步。并发后整体耗时接近最慢的单个仓库,而不是所有仓库耗时相加。

但库存扣减不能只靠多线程提升速度。扣减涉及同一 SKU 的可用量、锁定量和库存流水,必须通过数据库条件更新、行锁或乐观锁保证一致性:

1
2
3
4
5
6
UPDATE scm_inventory
SET available_qty = available_qty - #{qty},
locked_qty = locked_qty + #{qty}
WHERE warehouse_id = #{warehouseId}
AND sku_id = #{skuId}
AND available_qty >= #{qty};

业务代码要根据影响行数判断是否预占成功。

sleep、wait、join 的区别

sleep() 让当前线程暂停一段时间,但不会释放已经持有的对象锁。

wait() 必须在 synchronized 内调用,会释放对象锁,等待其他线程 notify()notifyAll()

join() 用于等待另一个线程执行结束。

示例:

1
2
3
Thread syncThread = new Thread(() -> inventorySyncService.syncWarehouse("WH-BJ"));
syncThread.start();
syncThread.join();

在业务系统里,更常用的是 CountDownLatchCompletableFuture、线程池和消息队列,而不是直接操作 wait/notify

常见风险

第一,线程过多。线程不是越多越快,线程数超过系统承载能力后,会导致上下文切换、内存压力、连接池耗尽。

第二,共享变量不安全。多个线程同时修改 HashMap、普通 int 或业务对象时,可能出现数据错乱。

第三,异常丢失。异步任务里的异常如果没有收集和记录,主流程可能误以为任务成功。

第四,业务一致性被破坏。订单状态、库存扣减、付款确认这类流程必须有明确事务边界。

小结

多线程基础不是背 API,而是理解线程状态、任务提交、协作方式和一致性边界。供应链系统中,仓库同步、物流轨迹拉取、报表计算适合并发;库存扣减、状态流转、财务结算必须优先保证一致性。

GitHub Pages + Hexo:个人技术博客的搭建与维护

个人技术博客的价值不只是“有一个能访问的网站”,更重要的是把学习、项目经验和问题复盘沉淀成可检索的资产。对于 Java 后端开发者来说,博客可以记录数据库锁、JVM、并发、供应链业务建模、AI 编程工具等内容,长期来看比零散笔记更容易形成体系。

这篇文章用当前项目的实际结构重新整理 Hexo + GitHub Pages 的搭建和维护流程。

整体流程

Hexo 博客发布流程

Hexo 适合解决什么问题

Hexo 是一个基于 Node.js 的静态博客框架。它把 Markdown 文章转换成 HTML、CSS、JS 等静态文件,再部署到 GitHub Pages。

这种方案的特点是:

  1. 内容用 Markdown 管理,适合技术文章。
  2. 静态页面访问快,部署成本低。
  3. 源码仓库和发布仓库可以分离,便于维护。
  4. 不依赖数据库,迁移和备份简单。

当前博客就是典型结构:

1
2
hexo_source 仓库:保存 Hexo 源码、文章、主题、配置
guxingxianyue.github.io 仓库:保存生成后的静态页面

平时写文章应该主要维护源码仓库,不要直接手改发布仓库里的 HTML。

本地启动流程

拉取源码仓库后,进入项目目录:

1
cd /Users/chenjinxing/IdeaProjects/mypy/myhexo

安装依赖:

1
npm install

本地启动:

1
npm run server

浏览器访问:

1
http://localhost:4000

如果只是验证静态生成是否正常,可以执行:

1
2
npm run clean
npm run generate

生成后的页面会放在 public/ 目录。

写一篇新文章

可以使用 Hexo 命令创建文章:

1
npm run new "MySQL事务隔离级别和MVCC"

也可以直接在 source/_posts 下创建 Markdown 文件。文章开头必须有标准 front matter:

1
2
3
4
5
---
title: MySQL事务隔离级别和MVCC:订单可见性怎么保证
date: 2025-04-17 09:50:00
tags: [MySQL, MVCC, 事务隔离级别, 供应链系统]
---

建议每篇技术文章都至少包含四部分:

  1. 背景:这个技术点解决什么问题。
  2. 原理:核心概念和工作机制。
  3. Demo:用代码或 SQL 说明。
  4. 场景:结合真实业务说明什么时候用、怎么避坑。

例如供应链系统里的库存预占文章,不应该只解释 SELECT ... FOR UPDATE,还要说明订单创建、库存锁定、扣减、释放和库存流水之间的关系。

内容质量检查清单

技术博客不要只追求“能发布”,更要保证读者能从文章里得到可复用的经验。每次写完文章,可以按下面的清单检查:

  1. 标题是否明确回答一个问题,而不是只写一个技术名词。
  2. 正文是否解释了为什么需要这个技术点。
  3. 是否给出可以运行或接近真实项目的代码、SQL、命令。
  4. 是否说明了适用场景和不适用场景。
  5. 是否把抽象概念落到订单、库存、采购、仓储、对账等业务对象上。
  6. 是否有流程图或结构图帮助读者快速建立整体认识。

例如写 “MySQL 行锁” 时,只说行锁、表锁、间隙锁是不够的。更好的写法是先给出库存预占场景,再说明为什么 WHERE sku_id = ? AND warehouse_id = ? 必须命中索引,最后补上死锁排查方式。这样文章才像工程复盘,而不是概念摘抄。

从旧电脑迁移时要注意什么

如果博客是从旧电脑迁移过来的,优先确认三个东西:

  1. 源码仓库是否完整,包括 source/_posts、主题、package.json_config.yml
  2. Node.js 版本和依赖是否能正常安装。
  3. 部署密钥或 GitHub 登录状态是否可用。

迁移后建议先不要直接部署,先执行:

1
2
3
npm install
npm run clean
npm run generate

如果生成成功,再启动本地服务:

1
npm run server

浏览器确认归档页、文章页、图片路径都正常后,再执行 npm run deploy。这样可以避免把本地缺图、front matter 错误、依赖缺失等问题发布到线上。

推荐的日常发布节奏

日常维护可以按这个顺序执行:

1
2
3
4
5
6
7
8
git pull origin master
npm run new "文章标题"
npm run generate
git status
git add source public db.json
git commit -m "Add xxx post"
git push origin master
npm run deploy

这里要注意,public/ 是生成结果,source/_posts 才是文章源文件。如果只是修改文章内容,先改 Markdown,再重新生成,不要直接改 public/ 里的 HTML。直接改生成结果,下一次 hexo generate 就会被覆盖。

部署到 GitHub Pages

确认本地生成成功后,执行部署:

1
npm run deploy

这个命令会执行:

1
2
3
hexo clean
hexo generate
hexo deploy

部署完成后,访问:

1
https://guxingxianyue.github.io

如果线上没有立即生效,通常是 GitHub Pages 还在构建。可以等待几十秒后刷新,或者查看 GitHub Pages 的 build 状态。

维护建议

博客长期维护时,建议遵守几个规则:

  1. 源码仓库保存文章和配置,发布仓库只保存生成结果。
  2. 每篇文章都写标准 front matter,方便归档、标签和后续迁移。
  3. 技术文尽量补流程图、代码示例和业务场景。
  4. 对过时文章保留发布时间,但可以在正文中说明“当前推荐做法”。
  5. 部署前执行 npm run generate,避免线上发现构建问题。

博客本质上是个人知识库。Hexo 只是工具,真正重要的是持续把项目经验写成结构化内容,并让每篇文章都能回答一个清晰的问题。