はじめに
株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
「Java用語、結局なんだっけ?」シリーズの第4回です。
| 回 | テーマ |
|---|---|
| #1 | 環境・基盤編 |
| #2 | DB接続編 |
| #3 | Web/サーバー編 |
| #4(本記事) | 例外・スレッド編 |
| #5 | モダンJava編 |
| #6 | フレームワーク編 |
| #7 | ビルド・運用編 |
今回は 例外・スレッド編 です。新人〜2年目が「雰囲気で使っているけど結局よく分からない」用語の最頻出ゾーンです。
この記事のゴール
- checked / unchecked 例外を区別できる
- try-with-resources の動きを説明できる
- スレッド、synchronized、volatile の役割を理解できる
- デッドロックがどう起きるか説明できる
① checked / unchecked 例外 ― 「コンパイラに強制されるか」
一言で言うと
| 種類 | 親クラス | コンパイラの扱い |
|---|---|---|
| checked 例外 |
Exception(RuntimeException 以外) |
catch または throws を強制 |
| unchecked 例外 | RuntimeException |
強制しない |
| エラー | Error |
通常はcatchしない(OOM等) |
階層図
Throwable
├── Error ← OutOfMemoryError等。catchしない
└── Exception
├── IOException ← checked(catch強制)
├── SQLException ← checked
└── RuntimeException ← unchecked(catch任意)
├── NullPointerException
├── IllegalArgumentException
└── IndexOutOfBoundsException
checked例外の挙動
// IOException は checked → catch または throws が必須
public void readFile(String path) throws IOException {
Files.readString(Path.of(path));
}
// または
public void readFile(String path) {
try {
Files.readString(Path.of(path));
} catch (IOException e) {
// 処理
}
}
throws も catch も書かないと コンパイルエラー になります。
unchecked例外の挙動
// NullPointerException は unchecked → 何もしなくてもコンパイルOK
public void process(User user) {
user.getName(); // user が null だと実行時にNPE
}
使い分けの目安
| 例外の意味 | 種類 |
|---|---|
| 業務上「起こりうる」失敗(例:残高不足) | checked(Spring Boot案件では unchecked も増えている) |
| プログラミングのバグ(例:nullを渡された) | unchecked(IllegalArgumentException等) |
| ファイル不在、ネットワーク不通 | checked(IOException系) |
詳しくは 「良いコード・悪いコード」シリーズ #8 例外処理 で扱っています。
② try-with-resources ― 「自動close機構」
一言で言うと
try (...) { } の括弧内で宣言したリソースを、ブロック終了時に自動でcloseする仕組み です。
Java 7 から導入。
悪い例(手動close)
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(path));
// 処理
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// 握り潰し
}
}
}
ネストが深く、null チェックも close の例外処理も必要。バグの温床。
良い例(try-with-resources)
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
// 処理
}
// ← ブロック終了時に自動でreader.close()される
対象となるリソース
AutoCloseable インターフェースを実装したクラスが対象です。
| 種類 | 例 |
|---|---|
| ファイル |
FileReader、BufferedReader、FileInputStream
|
| DB接続 |
Connection、PreparedStatement、ResultSet
|
| ストリーム |
InputStream、OutputStream
|
複数リソースの宣言
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
// 処理
}
// ← 宣言と逆順(rs → stmt → conn)でclose
よくある誤解
- 「try-with-resourcesは finally の置き換え」:close 処理だけが自動化される。finally節は今でも使える(必要に応じて併用)。
-
「自分で作ったクラスでも使える」:
AutoCloseableを実装すれば使える。
③ スレッド ― 「並列実行の単位」
一言で言うと
プログラム内で並列に動く実行の単位 です。
1つのアプリ内で複数のスレッドが同時に動くことができます。
コード例
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("t1: " + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("t2: " + i);
}
});
t1.start(); // ← スレッド起動(並列実行が始まる)
t2.start();
t1.join(); // ← t1が終わるのを待つ
t2.join();
}
}
t1 と t2 は 同時並行で動く ため、出力順は実行ごとに変わります。
スレッドの使われ方
| 場面 | 例 |
|---|---|
| Webリクエスト処理 | Tomcat が1リクエスト=1スレッドで処理 |
| 非同期処理 | バックグラウンドでメール送信 |
| 並列計算 | 大量データを分割して並列処理 |
Thread vs Runnable
// 1. Threadを継承(古いスタイル)
class MyThread extends Thread {
public void run() { /* 処理 */ }
}
new MyThread().start();
// 2. Runnableを実装(推奨)
Runnable task = () -> { /* 処理 */ };
new Thread(task).start();
// 3. ExecutorService(モダン)
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> { /* 処理 */ });
新規コードでは ExecutorService を使うのが現代的。スレッドを使い回せて効率的。
④ synchronized ― 「同時アクセスを禁止する鍵」
一言で言うと
複数スレッドから同じデータを同時に書き換えるのを防ぐ仕組み です。
ブロック単位 or メソッド単位で「同時に1スレッドしか入れない」エリアを作ります。
何が問題か(同期しない場合)
private static int counter = 0;
// 複数スレッドから同時に呼ぶと、counter が壊れる
static void increment() {
counter++; // ← この処理は「読み→加算→書き戻し」の3段階
}
counter++ は アトミック(不可分)ではない ため、2スレッドが同時に実行すると、片方の更新が消えることがあります。
良い例(synchronized)
public class ThreadDemo {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter = " + counter); // → 2000
}
static synchronized void increment() {
counter++;
}
}
synchronized メソッドは 同時に1スレッドしか実行できない ので、counter++ が壊れません。
同期方法の選択肢
| 方法 | 使いどころ |
|---|---|
synchronized メソッド |
メソッド全体を排他 |
synchronized ブロック |
必要な範囲だけ排他 |
ReentrantLock |
柔軟な制御(タイムアウト等) |
AtomicInteger 等 |
数値の単純な操作 |
並行コレクション(ConcurrentHashMap 等) |
並行アクセス対応のコレクション |
よくある誤解
- 「synchronized を付ければ全部安全」:違う。同じロックを使わないと意味がない。
- 「synchronized は遅い」:影響は限定的。正確性 > パフォーマンス が原則。本当に遅いなら最後に最適化する。
⑤ volatile ― 「変数の最新値を必ず見る」
一言で言うと
変数の値が、複数スレッドから常に最新の状態で見えるようにする修飾子 です。
何が問題か
JVMは最適化のため、変数の値をCPUキャッシュに保持することがあります。
すると、別のスレッドが変数を更新しても、別スレッドのキャッシュ上の古い値が読まれ続けることがあります。
コード例
public class VolatileDemo {
private static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
int count = 0;
while (running) { // ← volatile がないと、running の変更を見逃す可能性
count++;
}
System.out.println("worker停止。loop数: " + count);
});
worker.start();
Thread.sleep(100);
running = false; // ← メインスレッドから停止指示
worker.join();
}
}
volatile を外すと、ワーカースレッドが running = false を 永遠に検知しない 可能性があります。
volatile と synchronized の違い
| 観点 | volatile | synchronized |
|---|---|---|
| 用途 | 値の可視性保証 | 排他制御+可視性保証 |
| 対象 | 変数1個 | コード範囲 |
| パフォーマンス | 軽い | やや重い |
| 適用 | フラグ変数(ON/OFF) | 複数行の処理 |
よくある誤解
-
「volatile = synchronized の軽量版」:違う。volatile は排他制御をしない。
counter++には使えない。 - 「volatile を付ければ安全」:単純な読み書きのみ。複数ステップが絡む処理には不十分。
⑥ デッドロック ― 「お互いに永遠に待ち続ける状態」
一言で言うと
2つ以上のスレッドが、お互いのロック解放を待ち続けて永遠に動かなくなる状態 です。
発生例
Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(() -> {
synchronized (lockA) {
try { Thread.sleep(100); } catch (InterruptedException e) { /* 略 */ }
synchronized (lockB) { // ← t2 がロック中。待つ
// ...
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
try { Thread.sleep(100); } catch (InterruptedException e) { /* 略 */ }
synchronized (lockA) { // ← t1 がロック中。待つ
// ...
}
}
});
- t1 は lockA を持って、lockB を待つ
- t2 は lockB を持って、lockA を待つ
- 永遠に解放されない → デッドロック
防ぐ方法
| 方法 | 説明 |
|---|---|
| ロック取得順を統一 | 全スレッドが必ず「lockA → lockB」の順で取得 |
| tryLock を使う | 取得に失敗したら諦める |
| ロックの粒度を小さく | そもそも同時に複数のロックを取らない |
| タイムアウトを設定 | 一定時間待っても取れなければ抜ける |
よくある誤解
- 「デッドロックはスレッドが多いと起きる」:違う。2スレッドでも起きる。
- 「再起動すれば直る」:根本原因が残っていれば再発する。ロック設計の問題 として直す。
用語まとめ早見表
| 用語 | 一言で |
|---|---|
| checked 例外 | catch / throws を強制される例外(IOException等) |
| unchecked 例外 | 強制されない例外(RuntimeException系) |
| try-with-resources | リソースを自動close する構文 |
| スレッド | 並列実行の単位 |
| synchronized | 同時アクセスを禁止する鍵 |
| volatile | 変数の最新値が見えることを保証 |
| デッドロック | お互いのロック解放を永遠に待つ状態 |
現場あるある誤解集
| ❌ 誤解 | ⭕ 正しい理解 |
|---|---|
「catch (Exception e) で何でも捕まえる」 |
unchecked例外まで捕まえてしまうのでNG。具体的にcatch |
「finally でリソースをclose」 |
現代は try-with-resources を使う |
| 「synchronized = スレッドセーフ」 | 同じロックでないと意味がない |
| 「volatile = synchronizedの軽量版」 | 排他制御しない。可視性だけ保証 |
| 「Threadクラスを継承する」 | 現代は Runnable + ExecutorService が主流 |
| 「デッドロックは再起動で直る」 | 根本のロック設計を直す必要 |
「try-with-resources で finally は不要」 |
close以外の処理には finally が必要なケースもある |
演習問題
問題1:checked / unchecked ⭐
次のうち checked 例外 はどれですか?(複数選択可)
- A.
NullPointerException - B.
IOException - C.
IllegalArgumentException - D.
SQLException - E.
IndexOutOfBoundsException
模範解答
正解:B と D
解説:
-
B
IOException→ checked(Exception直系) -
D
SQLException→ checked - A、C、E は
RuntimeException系 → unchecked
ポイント:「RuntimeException を継承しているか」がunchecked判定の境界。階層図を頭に入れておくと判断できます。
問題2:synchronized と volatile ⭐
次のうち、volatile では解決できない問題 はどれですか?
- A. スレッドAが書き換えた値を、スレッドBが古いまま読んでしまう
- B. スレッドAとBが同時に
counter++を実行して、片方の更新が消える - C. 停止フラグ
boolean runningの変更を、別スレッドが検知できない
模範解答
正解:B
解説:
- A と C は volatile で解決可能:可視性の問題なので volatile が効く
-
B は volatile では解決不可:
counter++は「読み→加算→書き戻し」の3段階で、これがアトミックでない。synchronized またはAtomicIntegerが必要
ポイント:volatile は「最新値が見える」だけで、「同時に書き換えない」保証はない。
問題3:デッドロック ⭐
デッドロックを防ぐ方法として、最も効果的でない ものはどれですか?
- A. すべてのスレッドが同じ順序でロックを取得する
- B. ロックの粒度を細かくして、同時に複数のロックを取らないようにする
- C.
tryLockでタイムアウトを設定する - D. スレッドの数を減らす
模範解答
正解:D
解説:
- D は効果薄い:デッドロックは2スレッドでも起きるので、スレッド数を減らしても根本解決にならない
- A、B、C は有効な対策
ポイント:デッドロックは「ロックを取る順番」の問題。順番を統一すれば原理的に発生しない。
まとめ
例外・スレッド編の6用語のおさらいです。
- checked / unchecked 例外:コンパイラに強制されるかどうか
- try-with-resources:リソース自動close機構
- スレッド:並列実行の単位
- synchronized:同時アクセスを禁止する鍵
- volatile:変数の最新値を保証
- デッドロック:お互いに永遠に待つ状態
並行プログラミングは「正確性 > パフォーマンス」が大原則です。
新人のうちは 「複雑な同期は ConcurrentHashMap や AtomicInteger 等の高レベルAPIに任せる」 のが安全です。
次回予告
次回(#5)は モダンJava編 です。
- ラムダ式・メソッド参照・関数型インターフェース
- Stream API
- Optional
- record(Java 16+)
を解説します。
参考
- The Java® Tutorials - Concurrency
- The Java® Tutorials - Exceptions
- Effective Java 第3版 - 第10章 例外、第11章 並行性
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!