
本文介绍如何通过引入 hikaricp 连接池替代全局单数据库连接,实现多个 ui 后台任务真正并行执行数据库操作,避免长耗时查询阻塞其他快速查询。
本文介绍如何通过引入 hikaricp 连接池替代全局单数据库连接,实现多个 ui 后台任务真正并行执行数据库操作,避免长耗时查询阻塞其他快速查询。
在 JavaFX 应用中,使用 Task 在后台线程执行数据库操作是防止 UI 冻结的标准做法。但若所有数据库调用共享同一个 Connection 实例(如原代码中 DataHandler.conn 是静态单例),即使任务在不同线程中启动,它们仍会串行争用同一物理连接——因为 JDBC Connection 对象不是线程安全的,且 MySQL 单连接在同一时刻只能处理一个活跃请求。这就是为何 CALL 1(7 秒)未完成时,CALL 2(550ms)虽已进入 RUNNING 状态,却实际处于连接等待中,导致“假并行”。
根本解法是:让每个数据库操作独占一个连接,并由连接池统一管理资源。HikariCP 是高性能、轻量级的 JDBC 连接池,能高效复用连接、控制并发数、自动回收失效连接。
✅ 正确实践:基于 HikariCP 的并发查询
首先,在项目中添加 HikariCP 依赖(Maven):
<dependency><groupid>com.zaxxer</groupid><artifactid>HikariCP</artifactid><version>5.0.1</version></dependency>
然后重构 DataHandler,移除静态 Connection,改用 HikariDataSource 管理连接池:
public class DataHandler {
private static volatile DataHandler instance;
private static HikariDataSource dataSource;
private DataHandler() {
initDataSource();
}
private void initDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/database?serverTimezone=UTC");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(20); // 根据并发预期调整,通常 10–30 足够
config.setMinimumIdle(5);
config.setConnectionTimeout(30_000);
config.setIdleTimeout(600_000);
config.setMaxLifetime(1800_000);
// 关键优化:启用预编译语句缓存(提升重复 SQL 性能)
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("useServerPrepStmts", "true");
dataSource = new HikariDataSource(config);
}
public static DataHandler getInstance() {
if (instance == null) {
synchronized (DataHandler.class) {
if (instance == null) {
instance = new DataHandler();
}
}
}
return instance;
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection(); // 每次调用获取新连接(来自池)
}
// 数据库方法需显式获取并释放连接
public ObservableList<produitvendu> getProduitsVendusParPeriode(
LocalDate dateDebut, LocalDate dateFin,
boolean isEspece, boolean isDebit, String nomSearch) {
String sql = buildQuerySql(isEspece, isDebit);
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, "%" + nomSearch + "%");
stmt.setString(2, dateDebut.toString());
stmt.setString(3, dateFin.toString());
try (ResultSet rs = stmt.executeQuery()) {
ObservableList<produitvendu> result = FXCollections.observableArrayList();
while (rs.next()) {
result.add(new ProduitVendu(
rs.getInt("id_produit_vendu"),
rs.getInt("numero_bon"),
rs.getInt("reference"),
rs.getString("designation"),
rs.getInt("quantite"),
rs.getDouble("prix_vendu"),
rs.getDouble("benefice"),
rs.getDate("date_vente").toLocalDate(),
rs.getTime("heure_vente").toLocalTime()
));
}
return result;
}
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("Database query failed", e);
}
}
private String buildQuerySql(boolean isEspece, boolean isDebit) {
// ... 构建动态 SQL(同原逻辑,略)
return sql;
}
}</produitvendu></produitvendu>
? 关键点说明:
- try (Connection conn = getConnection()) 确保连接在作用域结束时自动关闭(归还至池),无需手动 close();
- 每个 Task 调用 getProduitsVendusParPeriode(...) 时,都会从池中获取独立连接,彼此互不阻塞;
- HikariDataSource 是线程安全的,可被任意数量线程并发调用 getConnection()。
✅ UI 层调用保持简洁(无需修改 Task 结构)
你的 loadHistoriqueVentesData() 方法完全无需改动——只需确保 db 实例是 DataHandler.getInstance() 返回的单例,其内部已使用连接池:
private void loadHistoriqueVentesData() {
// ... 参数校验(不变)
loadingHbox.setVisible(true);
Task<void> task = new Task() {
@Override
protected Void call() {
if (!searchString.isBlank()) {
// ✅ 此处调用已自动使用独立连接
ObservableList<produitvendu> data =
db.getProduitsVendusParPeriode(dateDebut, dateFin,
MHEspeceCheck.isSelected(),
MHDebitCheck.isSelected(),
searchString);
Platform.runLater(() -> {
MHHistoriqueTable.setItems(data);
MHHNbreResultatsText.setText(data.size() + " vente(s)");
});
}
return null;
}
};
task.stateProperty().addListener((obs, old, newState) -> {
if (newState == Worker.State.SUCCEEDED) {
loadingHbox.setVisible(false);
}
});
exec.execute(task); // 使用你原有的 Executor(推荐为 ForkJoinPool.commonPool() 或自定义线程池)
}</produitvendu></void>
⚠️ 注意事项与最佳实践
- 不要缓存 Connection:永远不要将 Connection 存为成员变量或静态字段;每次操作都应 getConnection() → 使用 → 自动关闭。
- 线程池配置匹配负载:Executor 的线程数建议 ≤ 连接池最大数(如 maximumPoolSize=20,则 exec 不宜超过 20 线程),避免连接耗尽。
- SQL 优化不可替代连接池:连接池解决并发瓶颈,但慢查询本身仍需索引、分页、执行计划分析等优化。
- 异常处理要完整:try-with-resources 已覆盖连接/语句/结果集的关闭,但业务异常(如空结果)需在 UI 层合理反馈。
- 连接泄漏检测:HikariCP 提供 leakDetectionThreshold(毫秒),启用后可记录未关闭连接的堆栈,开发期强烈建议开启。
通过以上改造,CALL 1 和 CALL 2 将真正并行执行:一个走连接 A 执行 7 秒查询,另一个立即获得连接 B 执行 550ms 查询,UI 响应速度与任务实际耗时对齐,彻底告别“伪异步”。











