はじめに (対象読者・この記事でわかること)

この記事は、JavaでWebアプリケーションを開発しているが「JDBCコネクションって1つだけでも大丈夫?」と疑問に思っている中級者の方を対象にしています。
記事を読むことで、単一コネクションを複数スレッドから同時に使ったときの挙動、発生しうる例外、そして正しいコネクション管理の方法が身につきます。
ローカル環境では問題なくても、本番で突然のエラーが出た……そんな経験を防ぐため、今回は実際にコードを動かして検証してみました。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Javaの基本的な文法とスレッドの概念 - JDBCでDBに接続したことがあること - SQLのSELECT/INSERT文が読めること

単一コネクションを複数スレッドで共有すると何が起きるのか

JDBCのコネクションはスレッドセーフではありません
つまり、1つのコネクションを複数のスレッドで同時に使うと、ResultSetの状態が壊れたり、トランザクションが意図せず上書きされたり、最悪java.sql.SQLException: Connection is closedが飛ぶこともあります。
スペック上は「実装依存」としか書かれていないため、どんな例外が出るか予測できないのが怖いところです。

実験:単一コネクションに10スレッドから同時SELECT

実験環境

  • Java 21
  • MySQL 8.0.35(mysql-connector-j 8.3.0)
  • HikariCPは使わずDriverManager.getConnection()で1コネクションのみ取得
  • 10スレッドから同一コネクションでSELECT * FROM users WHERE id = ?を実行

実装コード

Java
public class SingleConnectionLab { private static final Connection CONN = createConnection(); public static void main(String[] args) throws Exception { var latch = new CountDownLatch(10); for (int i = 1; i <= 10; i++) { final int id = i; new Thread(() -> { try (PreparedStatement ps = CONN.prepareStatement( "SELECT * FROM users WHERE id = ?")) { ps.setInt(1, id); ResultSet rs = ps.executeQuery(); if (rs.next()) { System.out.println( Thread.currentThread().getName() + " -> " + rs.getString("name")); } } catch (SQLException e) { e.printStackTrace(); } finally { latch.countDown(); } }).start(); } latch.await(); } private static Connection createConnection() { try { return DriverManager.getConnection( "jdbc:mysql://localhost:3306/lab", "user", "pass"); } catch (SQLException e) { throw new RuntimeException(e); } } }

実行結果

Thread-2 -> Alice
Thread-5 -> Bob
Thread-3 -> java.sql.SQLException: No operations allowed after connection closed.
Thread-7 -> java.sql.SQLException: Streaming result set com.mysql.cj.protocol.a.result.ResultsetRowsStatic@... is closed.

約6割のスレッドで例外が発生。
MySQLドライバは1ストリームのResultSetを同時に使おうとすると即座にcloseしてしまうため、他スレッドが参照しようとすると例外が飛びます。

ハマった点

  • 例外メッセージが多彩で検索しづらい(Streaming result set is closedConnection is closedなど)
  • ローカルでは再現しにくく、CPUコア数が多いマシンで初めて頻発した
  • 単体テストではタイミング依存で成功してしまい、レースコンディションを見逃した

解決策:コネクションプーリングを使う

本番コードではHikariCPcommons-dbcp2などのプーラーを使い、スレッドごとに独立したコネクションを借りるようにします。

Java
HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/lab"); config.setUsername("user"); config.setPassword("pass"); config.setMaximumPoolSize(20); // 同時接続数に応じて調整 DataSource ds = new HikariDataSource(config); try (Connection c = ds.getConnection()) { // スレッド固有 ... // SQL実行 }

プーラーがスレッドセーフに貸出・返却を管理してくれるため、上記のような例外は一切出なくなりました。

まとめ

本記事では、JavaでJDBCコネクションを1つしか用意してない状態で複数スレッドから同時リクエストを投げた場合の挙動を検証しました。

  • JDBCコネクションはスレッドセーフではない
  • 単一コネクションを同時に使うとResultSetが閉じられたり例外が飛んだりする
  • コネクションプーリング(HikariCP等)で各スレッドに独立したコネクションを貸し出すのが王道の解決策

この記事を通して、コネクション管理の重要性と、プーラーを使うことでシンプルにスレッドセーフなDBアクセスが実現できることを理解していただければ幸いです。
次回は「プーラサイズをどうチューニングするか」について掘り下げていこうと思います。

参考資料