CodeGym /コース /JAVA 25 SELF /同期でよくあるミスの解説

同期でよくあるミスの解説

JAVA 25 SELF
レベル 52, レッスン 4
使用可能

1. unlock/release の呼び忘れ: 不注意な人への落とし穴

ReentrantLock や Semaphore のような同期ツールを使うときの最も厄介なミスのひとつが、unlock() や release() の呼び忘れです。ロックを解放しなければ、他のスレッドはその解放を…永遠に待ち続けます。プログラムはハングし、なぜ何も起きないのか画面を見つめることになります。

ReentrantLock を使った例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        // あっ! unlock() を忘れた - これで全員が停止する!
        count++;
    }
}

一見何も問題なさそうですが、increment() を複数スレッドから何度か呼ぶと、最初の呼び出し以降、他のスレッドはロックが解放されるのを無限に待ち続けます。

この事態を避けるには try-finally を使います:

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

これで、メソッドの途中で例外が発生しても、ロックは必ず解放されます。

たとえるなら、誰かがトイレを占有して(内側から鍵を掛けて)おきながら、ドアを開け忘れて窓から出てしまったようなものです。他の人はその人が出てくるのをずっと待つことになります…そんなことはしないでください!

2. 間違ったオブジェクトでの同期: 「あっ、違う所にロックを掛けちゃった!」

Java のキーワード synchronized は、あるオブジェクトへのアクセスをブロックできます。しかし、ロック対象のオブジェクトを間違えると、期待どおりに同期されません。

誤り1: ローカル変数での同期

public void doSomething() {
    Object lock = new Object();
    synchronized (lock) {
        // 毎回新しいオブジェクトになる - 同期はまったく効かない!
        // スレッドは互いに待たない。
        // クリティカルセクションは保護されていない!
    }
}

ここでは各スレッドがそれぞれ独自の lock オブジェクトを作っています。その結果、実質的なロックは行われず、スレッドは同時にクリティカルセクションへ入ってしまいます。

正しいやり方:

private final Object lock = new Object();

public void doSomething() {
    synchronized (lock) {
        // これで全スレッドが同じオブジェクト lock を使い
        // 互いにきちんと待つようになる。
    }
}

誤り2: 文字列リテラルで同期する

public void doSomething() {
    synchronized ("lock") {
        // 文字列リテラルはインターンされるため、プログラムの別の箇所が
        // 偶然同じ文字列で同期してしまう可能性がある!
    }
}

結論:
同期は、他で使われない専用のプライベートなオブジェクトに対してのみ行いましょう。

3. 相互待ち(deadlock): 「君が先に、いや君が先に」で双方停止

Deadlock(相互待ち)は定番の問題です。2つ(以上)のスレッドが別々のロックを順に取得して互いを待ち、プログラムが固まってしまいます。

例:

public class DeadlockExample {
    private final Object lockA = new Object();
    private final Object lockB = new Object();

    public void method1() {
        synchronized (lockA) {
            // 動作を再現しやすくするために少し待つ
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            synchronized (lockB) {
                // ...
            }
        }
    }

    public void method2() {
        synchronized (lockB) {
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            synchronized (lockA) {
                // ...
            }
        }
    }
}

一方のスレッドが method1() を呼び、もう一方が method2() を呼ぶと、前者は lockA を取得して lockB を待ち、後者はその逆を行います。結果として双方が永遠に相手を待ち続けます。

回避方法:

  • すべてのスレッドで、常に同じ順序でロックを取得する。
  • 同時に保持するロックの数を最小限にする。
  • プログラムが固まった場合は診断ツール(たとえば jstack)を使う。

たとえ:
狭い廊下で二人が出会い、相手が先に譲るなら自分も譲ると考えている状況に似ています。結局どちらも動かず、誰かが先に折れるまで待ち続けます。

4. 過剰な同期: 「念のためロックは多めに?」— いつも正しいとは限らない!

バグを恐れて何でもかんでも同期してしまうことがあります。その結果、性能は落ちるのに得られる利益はゼロです。

例:

public synchronized void add(int value) {
    // ここには同期不要の1行しかない!
    System.out.println("追加: " + value);
}

この場合、同期は不要です。System.out.println による出力はすでにスレッドセーフであり、このメソッド自体も共有リソースを扱っていません。

どこが問題になる?
頻繁に呼ばれ、保護の不要なメソッドまで同期してしまうと、プログラムの性能は大きく低下します。本来並行して動けるはずのスレッドが、わざわざ列を作って待つことになるからです。

ベストプラクティス:
本当に必要な部分だけを同期する。クリティカルセクションは可能な限り小さく保つ。

5. volatile の誤用: 「可視性はあるが、原子性はない!」

Java の volatile 修飾子は、変数の変更がすべてのスレッドから見えることを保証します。しかし、原子性は保証しません。

誤り:

private volatile int counter = 0;

public void increment() {
    counter++; // 原子操作ではない!
}

counter++ は、読み取り・加算・書き戻しから成る複合操作です。2つのスレッドが同時にこのコードを実行すると、最終的な値が期待より小さくなることがあります。

正しいやり方:
原子操作が必要なら synchronized、AtomicInteger などのスレッドセーフなクラスを使いましょう。

import java.util.concurrent.atomic.AtomicInteger;

private final AtomicInteger counter = new AtomicInteger();

public void increment() {
    counter.incrementAndGet();
}

volatile を使うのはいつ?
単純なフラグ(例: 「終了する」)のように原子性が不要な場合です。

1
タスク
JAVA 25 SELF, レベル 52, レッスン 4
ロック未解除
信頼性の高いトランザクション記録システム 💳
信頼性の高いトランザクション記録システム 💳
1
タスク
JAVA 25 SELF, レベル 52, レッスン 4
ロック未解除
中央管理のオフィスプリンター 🖨️
中央管理のオフィスプリンター 🖨️
1
アンケート/クイズ
スレッドの同期、レベル 52、レッスン 4
使用不可
スレッドの同期
スレッドの同期
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION