The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目71:遅延初期化を注意して使用する

遅延初期化(lazy initialization)は、フィールドの値が必要になるまで初期化を遅らせる手法です。 static フィールドとインスタンスフィールドの両方に利用できます。

まず、第一に、通常は遅延初期化を使うべきではありません。 初期化を遅らせる仕組みによって、フィールドへのアクセスコストが増加するからです。

遅延初期化を採用する理由は間違いなく「最適化」が目的ですが、項目55で述べたように多くの「最適化」は失敗します。 パフォーマンスを測定し、ほんとうに遅延初期化が必要かどうかを必ず判断してください。

マルチスレッド環境の遅延初期化

通常の初期化

遅延初期化しない通常の初期化は次のように行われます。

private final FieldType field = initializeField();

これは static フィールドの場合も同様です。 final キーワードがついていることが重要です。

final キーワードはマルチスレッド環境においても安全にフィールドが初期化されることを保証するからです。

通常の遅延初期化

次に最も一般的な遅延初期化の方法を紹介します。

private FieldType fieldType;
public synchronized FieldType getField() {
    if (field == null) {
        field = initializeField();
    }
    return field;
}

これは static フィールドでも、インスタンスフィールドでも有効です。 synchronized キーワードによって適切に同期化されていることが重要です。

最適化された遅延初期化

もし、単に同期化しただけの遅延初期化でパフォーマンス上の問題が発生するならば、いくつかの選択肢があります。

遅延初期化ホルダークラス

static フィールドの初期化には遅延初期化ホルダークラスイディオムを使うと効果的です。

private static class FieldHolder {
    static final FieldType field = initializeField();
}

static public FieldType getField() {
    return FieldHolder.field;
}

これは『クラスが利用されるまでクラスが初期化されない』という言語仕様を利用したイディオムです。

ダブルチェックイディオム

遅延初期化ホルダークラスはシンプルで効果的な実装ですが、インスタンスフィールドには適用できません。

インスタンスフィールドにはダブルチェックイディオムと呼ばれる遅延初期化が有効です。

private volatile FieldType field;

public FieldType getField() {
    FieldType result = field;
    if (result == null) {
        synchronized(this) {
            result = field;
            if (result == null) {
                field = result = initializeField();
            }
        }
    }
    return result;
}

synchronized で同期化する前に初期化されているかどうかをチェックし、この時点で初期化されていたらそのまま result を返します。 こうすることでほとんどの場合、同期化を避けて値を返すことが可能です。

もし、初期化されていない場合には念のために synchronized を使って同期化し、もう一度チェックします。 このように二重にチェックするためにダブルチェックイディオムと呼ばれています。

フィールドが volatile であることは非常に重要です。 また、一度、ローカル変数に代入することでパフォーマンスが改善します。

このイディオムは Java 5 未満のメモリセマンティクスでは動作が保証されないことに注意が必要です。

hjm333.hatenablog.com

単一チェックイディオム

もし、特に高いパフォーマンスが求められる場合で、二回以上初期化しても問題ないのであれば synchronized を外してもかまいません。

private volatile FieldType field;

public FieldType getField() {
    FieldType result = field;
    if (result == null) {
        field = result = initializeField();
    }
    return result;
}

きわどい単一チェックイディオム

もし、long と double 以外の基本データ型であれば、単一チェックイディオムから volatile を取り除いてもかまいません。

private int field;

public int getField() {
    int result = field;
    if (result == null) {
        field = result = initializeField();
    }
    return result;
}

この手法では、新しいスレッドがアクセスするたびに initializeField() が呼ばれる可能性があります。 スレッドは、volatile 修飾子がなくても自分のスレッドが行った変数の変更は見ることができる一方、他のスレッドの変更は見ることができない可能性があるからです。

これはかなりきわどい手法ですが意図通りに動作します。 実際、String インスタンスで hashCode 値をキャッシュするために利用されています。

【Effective Java】項目70:スレッド安全性を文書化する

開発者は、クラスがマルチスレッド環境で利用された場合にどのように振る舞うかを文書化しなければなりません。

マルチスレッド環境における振る舞いを文書化しない場合、クラスの利用者は十分な同期を行わない(項目66)かもしれませんし、逆に過度な同期を行う(項目67)かもしれません。

synchronized 修飾子を手がかりにしてクラスのスレッドセーフ性を確認することは大きな誤りです。 synchronized 修飾子は API の仕様ではなく実装の詳細だからです。

synchronized 修飾子があるからといってスレッドセーフは保証されませんし、synchronized 修飾子がなくてもスレッドセーフ性が保証される場合もあります。

スレッドセーフレベル

スレッドセーフには次のようなレベルがあります。

  • 不変(immutable)
  • 無条件スレッドセーフ(unconditionally thread-safe)
  • 条件付きスレッドセーフ(conditionally thread-safe)
  • スレッドセーフでない(not thread-safe)
  • スレッド敵対(thread-hostile)

不変

スレッドセーフレベルが不変の場合、クラスのインスタンスは不変であるため、外部同期は一切必要ありません。 String, Integer, BigInteger などがその例です。

無条件スレッドセーフ

スレッドセーフレベルが無条件スレッドセーフの場合、クラスは可変です。 しかし、すべてのパブリックメソッドはスレッドセーフであることが保証されます。

クラスは適切に内部同期されているため、外部同期は一切必要ありません。 例としては、ConcurrentHashMap などです。

無条件スレッドセーフを実現したい場合、プライベートロックイディオムを使うことが効果的です。

private final Object lock = new Object();
public void foo() {
    synchronized(lock) {
        ....
    }
}

クライアントがロックオブジェクトに触ることができると、悪意あるクライアントによってサービス拒否攻撃が行われる可能性があります。 このイディオムを用いれば、外部からロックオブジェクトに触ることが出来なくなり、この攻撃を防ぐことができます。

また、プライベートロックイディオムは継承のために設計されたクラスに対して有効です。

条件付きスレッドセーフ

このレベルの場合、クラスは可変であり、いくつかのメソッドは適切な外部同期を必要とします。 例えば Collections.synchronized ラッパーが返すコレクションです。

Collections.synchronized ラッパーが返すコレクションのイテレータを利用したい場合、次のような外部同期が必要です。

Map<K, V> map = Collections.synchronizedMap(new HashMap<K, V>()));
Set<K> set = m.keySet(); // ここでは同期は必要ない
// イテレータを利用する場合、map に対するロックが必要
synchronized (map) {
    for (K key: set) { 
        ....
    }
}

このように、条件付きスレッドセーフレベルではクライアントにロックオブジェクトを渡す必要があります。 そのため、条件付きスレッドセーフではプライベートロックイディオムを使うことはできません。

スレッドセーフでない

このレベルでは、クラスは可変ですべてのメソッドが外部同期が必要です。 例えば、ArrayList や HashMap などです。

スレッド敵対

このレベルのクラス/メソッドは、たとえ外部同期されていてもマルチスレッドで利用することは安全ではありません。

このクラスは Java ライブラリにもほとんどありません。 希有な例としては System.runFinalizersOnExit() です。

感想

System.runFinalizersOnExit() がなぜだめなのか調べた。

まずはリファレンスをチェック。

Runtime (Java Platform SE 7)

非推奨。 このメソッドは本質的に安全ではありません。ファイナライザがライブオブジェクトに対して呼び出される結果になる可能性があり、そのときにほかのスレッドがそれらのオブジェクトを並行して操作していると、動作が異常になるか、デッドロックが発生します。   終了時のファイナライズを有効または無効にします。これを実行することによって、自動的に呼び出されていないファイナライザを持つすべてのオブジェクトのファイナライザが呼び出され、Java Runtime の終了前に実行されるようになります。

なるほど、これをオンにすると終了時に各オブジェクトの finalize() メソッドが必ず呼び出されるようになる。 この時、そのオブジェクトが並行に実行されていたりする可能性があるので危険、ということだな。

【Effective Java】項目69:wait と notify よりコンカレンシーユティリティを選ぶ(後半)

固有のロック

Java は相互排他ロックを言語固有の機能として持っています。 このロックを利用するためには synchronzied ブロックを使います。

固有のロックは次のような仕組みで動作します。

  1. synchronized ブロックに入る時、スレッドは指定されたオブジェクトの固有のロックを自動的に取得します
    1. スレッドがロックを取得できたら、ブロック内の処理を実行します
    2. 他のスレッドがロックを取得している場合、ロックが取得できるまでスレッドの実行は待機します
  2. synchronized ブロックから出る時、スレッドは指定されたオブジェクトのロックを自動的に解放します
  3. 当該オブジェクトのロック取得待ちをしていたスレッドの1つがロックを取得し、後続の処理を実行します

例えば次のようなコードを2つのスレッドAとBで実行するとします。

synchronized (obj) { // obj は Object 型のインスタンス
    ...処理A...
    ...処理B...
}
  1. まず、スレッド A がこのコードブロックを実行します。その際、スレッド A が obj の固有のロックを自動的に取得します。
  2. この時、スレッド B がこのコードを実行しようとします。その際、スレッド B は obj の固有のロックを自動的に取得しようとします。ただし、スレッドAによってロックは占有されていますので、解放されるまで待機します。
  3. この間に スレッド A は処理Aと処理Bを実行します。synchronized ブロックを抜ける時、スレッド A は obj の固有のロックは解放します。
  4. すると、待機していたスレッドBが解放されたロックを取得します。最後に、スレッドBはコードを実行し、ブロックをぬけた時に obj の固有のロックは解放されます。

なお、同じスレッドは同じオブジェクトのロックを取得することができます。 これを再突入可能といいます。

wait() と notify()

wait() と notify() は Java のスレッド間の調整を行うためのメソッドです。 より正確には条件キュー(Condition Queue)と呼ばれる機構を操作するための API です。

Java のすべてのオブジェクトは条件キューを持っていて、Object クラスの wait() と notify() メソッドによってこのキューを操作することができます。

Object.wait() を呼び出すと、呼び出したスレッドはそのオブジェクトの条件キューに入ります。 そして、そのスレッドはそこでブロックします。

Object.notify()、または Object.notifyAll() を呼び出すと、そのオブジェクトの条件キューに入っているどれかのスレッドのブロックを解放します。 どのスレッドが解放されるかは保証されません。

wait()、notify()、notifyAll() を呼び出すためにはロックが必要です。

synchronized(obj) { // obj のロックを取得する必要がある
    obj.wait(); // スレッドは条件キューに入り、スレッドの動作は止まる。そして、wait() はロックを解放する
}

一方、notify() や notifyAll() は条件キューに入ったスレッドを解放し、wait() の待ちを解除します。

synchronized(obj) { // obj のロックを取得する必要がある
    // 条件キューに含まれているすべてのスレッドのブロックを解放する
    obj.notifyAll();
}

wait() と notify() のイディオム

前述のように wait() と notify() は Java の条件キューを操作する低レイヤな API です。 wait() と notify() を正しく使うために必要なイディオムを次の通りです。

  • Object.notify(), Object.wait() を呼ぶ時はロックを取得します。
  • while ループで条件が満たされてない場合はループさせます
    • while ループ外で wait() を呼び出してはいけません
  • なるべく notifyAll() を使うべきです。

なぜなら、次のようなケースが考えられるからです。

  • 条件が満たされた状態で wait() に入ってしまうと、notify() が永久によばれない可能性がある
    • このために do-while ではなく while を使う必要があります
  • 条件が満たされていないのに wait() が目覚める可能性がある
    • このために if ではなくループを使う必要があります
  • 他の場所で notifyAll() が使われると、すべての wait() が目覚める
  • 待ちスレッドは希に偽りの目覚めを起こす場合がある。
  • 他のスレッドが悪意をもって notify を呼び出す可能性がある。

このイディオムを擬似コードにすると次のようになります。

// wait を利用するための標準イディオム
synchronized (obj) {
        while (<条件が満たされていない>) {            
            obj.wait();
        }
        // 満たされた条件に対して適切な処理を行う
    }
}

一般的にスレッドを起こす際には notifyAll() を使うべきですが、注意深く使えば notify() の方がパフォーマンスが少し高いです。

このように wait() や notify() を使うためには複雑なイディオムが必要となるので、通常は java.util.concurrent の並行ライブラリをそのまま利用するべきです。

自分を使い切る

元陸上選手の為末さんがこんなことを書いていた。

http://tamesue.jp/blog/archives/think/20160924

幸福というのは自分はどこまでいったかというよりも、自分という存在をどこまで使い切れたか、能力を引き出し切れたかによるのではないかと思うようになった。

もうすぐ30歳(自分が)。

20歳の頃、30歳の自分はもっと成功していて、もっと遠くにいると思ったのだけれど、そんなことはなかった。

最近、

「20歳の自分に『なんだ、まだこんなところにいるのか』と言われてしまうんだろうなぁ」

と、よく思う。

自分がどこまでいけたかではなく、自分を使い切れたかどうかが幸福を左右する

言われてみればたしかにその通りかもしれない。人生において自分を使い切れた瞬間には輝きがあったように思える。

自分はどこまできたのか

この思考から離れることができれば、今の生活にもう少し明かりが見えるのかもしれない(別に暗いわけじゃないのだが)。

なんだかポエム感がすごいが、たまにはこういう記事も悪くない。

【Effective Java】項目69:wait と notify よりコンカレンシーユティリティを選ぶ(前半)

Java のスレッド操作 API として Object.wait() と Object.notify() があります。 しかし、Java 1.5 からはより高いレベルのユティリティが導入されたので wait() と notify() は使うべきではありません。

追加されたのは java.util.concurrent ユティリティです。 concurrent ユティリティは次の3つに分類されます。

エグゼキューターフレームワークについてはすでに項目68で簡単に説明しました。

今回はコンカレントコレクションとシンクロナイザーについて説明します。 また、wait() と notify() を正しく使う方法についても説明します。

コンカレントコレクション

コンカレントコレクションは、List や Map などのコレクションの高パフォーマンスな実装です。 高度な並行性が実装されていて、すべて内部同期されているクラスです。

内部的に同期されているため、これらのクラスから並行性を除去することはできません。 また、外部から追加でロックをすることに意味はありません。 単にプログラムを遅くするだけです。

ConcurrentHashMap

コンカレントコレクションの複数のメソッド呼び出しを外部同期によってアトミックにすることはできません。 代わりに一部のコレクションインタフェースは、アトミックな単一メソッドを提供しています。

たとえば、ConcurrentMap には「指定されたキーをチェックする操作」と「指定されたキーに値を紐付ける操作」をアトミックに行う putIfAbsent() メソッドがあります。

Java 1.5 以前には同期化されたマップの実装として SynchronizedMap がありました。 Java 1.5 以降は、基本的に SynchronizedMap ではなく ConcurrentHashMap を利用するべきです。 ConcurrentHashMap の高度な並行性実装によって、単に SynchronizedMap を ConcurrentHashMap に置き換えるだけで劇的にパフォーマンスが改善する可能性があります。

より一般的には、外部同期されたクラスよりも内部同期された適切なクラスを使うべきです。

ブロック操作

コンカレンシーユティリティのいくつかのインタフェースは操作が完了するまでスレッドを待機させるブロック操作を持っています。

たとえば、BlockingQueue は Queue を拡張しており、take メソッドが追加されています。 このメソッドは「キューの先頭から値を取り出す。ただし、空であれば待機する」という処理を行います。

このクラスはスレッドプールのワークキュー実装に最適です。 実際、エグゼキューターフレームワークのほとんどがこの BlockingQueue を利用しています。

シンクロナイザー

シンクロナイザーは「他のスレッドの完了を待つ」といった処理を可能にするクラスです。 より一般的にはスレッド間の活動を協調するために利用します。

代表的なシンクロナイザーは CountDonwLatch と Semaphore です。 Java 1.5 より前では wait() と notify() を使ってスレッド間の協調を行っていました。 現代においてそれは正しい選択肢ではありません。

CountDownLatch は複数のスレッドの処理が終わることを待ち合わせるために利用できます。 例えばいくつかのタスクの完了を待ち合わせる処理を CountDownLatch を使って実装すると次のようになります。

public static void run(Executor executor, int taskNum) throws InterruptedException {
    // タスク数と同一のカウントを持つラッチを作成
    final CountDownLatch latch = new CountDownLatch(taskNum);
    for (int i = 0; i < taskNum; i++) {
        final int n = i;
        executor.execute(new Runnable() {
            @Override public void run() {
                System.out.println("task#"+ n);
                // ラッチを一つさげる
                latch.countDown();
            }
        });
    }
    // すべてのタスクが終了するのを待つ
    latch.await();
}

実はこの処理を正しく wait() と notify() で実装するのはかなり難しいです。

次回は wait() と notify() の正しい利用方法について説明します。

【Effective Java】項目68:スレッドよりエグゼキューターとタスクを選ぶ

Java 1.5 から、エグゼキューターフレームワークが利用できるようになりました。 このフレームワークを使うと、スレッドで実行するためのワークキューを簡単に作成することができます。

// スレッドで実行するためのエグゼキューターサービスの生成
ExecutorService executorService = Executors.newSingleThreadExecutor();

他のスレッドで処理を実行するには次のように書きます。

// Runnable の実行
executorService.submit(new Runnable() {
        @Override public void run() {
            // 他のスレッドで実行される
            System.out.print("run");
        }
   });

実行中の処理が終わるのを待ち、エグゼキューターを終わらせるコードは次のようになります。

executorService.shutdown();

このようにエグゼキューターフレームワークを使うことで、スレッドで実行する処理をかなり簡単に書くことができるようになりました。

柔軟性

エグゼキューターサービスは従来のスレッドクラスにはなかった次のような機能を持っています。

  • 特定のタスクの完了を待つ
  • タスクの集まりの一部/すべての終了を待つ
  • タスクが完了するごとに結果を取り出す

スレッドプール

キューを実行するスレッドを2本以上持つ『スレッドプール』も簡単に利用可能です。

小さく負荷の低い処理を行う場合には、Executors.newCachedThreadPool() を使うとよいでしょう。

このスレッドプール実装ではタスクの実行が要求されると、新しいスレッドを生成します。 もし、タスクを処理していないスレッドがあれば、そのスレッドは再利用されるます。

ただし、一度にたくさんのタスクが投入されるとその数だけスレッドを生成してしまうため、リソースが枯渇する可能性があります。 一度に大量のタスクが投入されることが想定されるのならば、最大スレッド数が制限された Executors.newFixedThreadPool() を使うべきです。

Thread の利用は避ける

エグゼキューターフレームワークによって自分でワークキューを書く必要性はなくなりました。 それに加えて、直接 Thread クラスを利用する必要もほとんどありません。

従来の Thread クラスのインタフェースには「機能」と「機構」が混在しています。 エグゼキューターフレームワークでは「機能」と「機構」は分離されています。

機能に対応するクラスは「タスク」と呼ばれています。 具体的には Runnable インタフェースと Callable インタフェースが対応します。 Runnable は値を返しませんが、Callable は値を返すことができます。

一方、機構に対応するのがエグゼキューターです。 機構はタスクである Runnable や Callable を実行するためのフレームワークです。

ScheduledThreadPool

エグゼキューターフレームワークは従来の Timer クラスの代わりにもなります。 従来の Timer は実行スレッドが一つしかないため、次のような欠点がありました。

  • 長い時間動作するタスクがある場合にタイマーの精度が悪くなる
  • 唯一のスレッドが例外を投げると Timer 全体が動作しなくなる。

エグゼキューターフレームワークの ScheduledThreadPoolExecutor はこれらの問題を解決しています。

ScheduleThreadPoolExecutor は複数スレッドをサポートしています。 また、タスクからチェックされない例外がスローされた場合にも、実行スレッドは正しく回復します。

【Effective Java】項目67:過剰な同期は避ける

前回の記事では、同期が不十分な場合にはマルチスレッド環境において予期しないエラーが発生する可能性を説明しました。

hjm333.hatenablog.com

hjm333.hatenablog.com

今回は前回とは逆の視点です。 過剰に同期することによって発生する問題について取り上げます。

過剰な同期による問題

Java において過剰に同期を行うと次の問題が発生する可能性があります。

まずはデッドロックを引き起こす可能性のあるコードについて説明します。

オープンコール

synchronized で同期しているメソッドやブロック内で、制御を他のコードに譲ってはいけません。

例として、リスナーを介して追加の通知が取得できるセットの実装(ObservableSet)を取り上げます。 この実装は項目16で利用した ForwardingSet を利用しています。

/**
 * クライアントが要素を追加した時に、通知を受け取ることが可能。
 * Observer パターンを利用。
 */
public class ObservableSet<E> extends ForwardingSet<E> {
    interface SetObserver<E> {
        void added(ObservableSet set, E element);
    }

    public ObservableSet(Set<E> set) {
        super(set);
    }

    private final List<SetObserver<E>> observers = new ArrayList<>();

    public void addObserver(SetObserver<E> observer) {
        synchronized (observers) {
            observers.add(observer);
        }
    }

    public boolean removeObserver(SetObserver<E> observer) {
        synchronized (observers) {
            return observers.remove(observer);
        }
    }

    private void notifyElementAdded(E element) {
        synchronized (observers) {
            for (SetObserver<E> observer: observers) {
                observer.added(this, element);
            }
        }
    }

    @Override
    public boolean add(E element) {
        boolean added = super.add(element);
        if (added) {
            notifyElementAdded(element);
        }
        return added;
    }
}

この Set は任意のリスナーを登録できます。 要素が追加された際は、そのリスナーを通じて通知を受け取ることができます。

問題は通知を発生させる notifyElementAdded() です。 このメソッドは同期化されており、SetObserver.added() を同期ブロック内で呼び出しています。

SetObserver.added() の実装について ObservableSet は何も知りませんので、どんな処理が行われるか保証できません。 すごく時間のかかる処理をするかもしれないですし、例外を発生させる処理かもしれません。

もし、次のようなコードが書かれているとしたらプログラムはデッドロックしてしまいます。

set.addObserver(new SetObserver<Integer>() {
    @Override
    public void added(final ObservableSet set, Integer element) {
        if (element == 23) {
            ExecutorService executor =
                    Executors.newSingleThreadExecutor();
            final SetObserver<Integer> observer = this;
            // 要素の削除をバックグラウンドスレッドで実行
            try {
                executor.submit(new Runnable() {
                    @Override
                    public void run() {
                        set.removeObserver(observer);
                    }
                });
            } finally {
                executor.shutdown();
            }
        }
    }
});

このデッドロックは SetObserver.added() を同期ブロック内で呼び出しているために発生します。

同期ブロック内で呼び出しなので SetObserver.added() の呼び出しスレッドはロックを取得している状態です。 そのうえで、他のスレッドで ObservableSet.removeObserver() を呼び出そうとしています。

removeObserver() は同期化されているため、SetObserver.added() を呼び出したスレッドが持っているロックを待つ必要があります。 しかし、removeObserver() が終わらない限り SetObserver.added() の処理は終わらないため、ロックは永遠に解放されません。 すなわち、removeObserver() を呼び出したスレッドはロックの取得を永遠に待ち続けるのです。

これがデッドロックの原因です。 このように同期ブロック内で異質なメソッドを呼び出すと、意図しないデッドロックを発生させる可能性があります。

最小限の同期

このような問題を防ぐためのプラクティスを紹介します。 それは「同期された領域内で行う処理は最小限にするべき」というものです。 同期ブロックに入ったら必要な共有データのみを調べ、そして同期ブロックを抜けるのが理想です。

先ほどの ObservableSet では同期ブロック内で SetObserver.added() を呼び出していることが問題でした。 そこで次のようにコードを変更します。

private void notifyElementAdded(E element) {
    List<SetObserver<E>> snapshot = null;
    synchronized (observers) {
        // 同期ブロック内ではその時点で observer のコピーを保存
        snapshot = new ArrayList<SetObserver<E>>(observers);
    }
    for (SetObserver<E> observer: snapshot) {
         observer.added(this, element);
    }
}

snapshot にコピーを保存して、異質なメソッド(SetObserver.added())を同期ブロックで呼ばないように変更しました。 こうすればオープンコールは同期化ブロック外で呼び出されますので、デッドロックを回避できます。

Java ライブラリには CopyOnWriteArrayList と呼ばれるスレッドセーフなリストが実装されています。 これを使っても問題ありません。

パフォーマンス

現在の JVM はロックの取得のためには CPU 時間をあまり消費しません。 その代わり、メモリの一貫性を保つために遅延が発生します。 また、同期化によって JVM 上の最適化が除去されてしまいます。

そのため、過剰な同期はパフォーマンスを低下させます。 パフォーマンス上の視点でも過剰な同期は避けるべきです。

外部同期・内部同期

クラスがマルチスレッド環境で利用されるのならば、クラスの内部で同期を行ってクラス自体をスレッドセーフにするべきです。

クラスを内部的に同期するようにすると、次のようなテクニックを使って高い平行性が実現できます。

しかし、内部同期を使った結果として高いパフォーマンスが得られないのであれば、内部同期しないほうがましです。 クラスでは同期を行わずにそのクラスを利用する外部のクライアント側で同期を行うべきです。

Java の初期の頃は内部同期を使っているクラスが多くありました。 たとえば StringBuffer はその代表例です。

しかし、StringBuffer にスレッドセーフ性が必要な場合はほとんどありません。 そこで Java 1.5 からは内部同期を行わない StringBuilder が導入されました。 「必要であれば StringBulder を使うクライアントが外部から同期を行うべき」という考えです。

このように、必要な場合のみ内部的にスレッドセーフを実現するべきです。

(c) The King's Museum