よしたろうブログ

設計・人文知・歴史・哲学・漫画とかの話が好きです。

static と synchronized から始めるマルチスレッド入門 〜前編:static とメモリ管理〜

初めに

本記事は3部構成になっております。

  • 『前編:static とメモリ管理』

static と synchronized から始めるマルチスレッド入門 〜前編〜 - よしたろうブログ

  • 『中編:マルチスレッドの基本的な用語と概念』

static と synchronized から始めるマルチスレッド入門 〜前編〜 - よしたろうブログ

  • 『後編:synchronized の使い方と注意点』

static と synchronized から始めるマルチスレッド入門 〜後編〜 - よしたろうブログ

本記事では実務の中で使用している静的解析ツール(SonarLint)に指摘された警告の修正を入り口に、マルチスレッドについて説明したいと思います。

本記事では、以下の事柄について解説していきます。

  • static の意味
    • メモリ管理の方法
    • メモリ上にプログラムを展開する意味と理由
    • atatic とインスタンスの違い
    • static の使い所と注意点
  • マルチスレッドの基本的な話
    • プロセスとは
    • スレッドとは
    • スレッドセーフとは
    • 並行と並列
  • 同期制御と排他制御の概論と基本
  • アトミック性と可視性
  • synchronizedとは
    • インクリメントは非アトミックな操作
    • synchronized はミューテックス(単一ロック)
    • ミューテックスはパフォーマンスには良くない
    • synchronized メソッドと synchronized ブロックの違い
    • その他の注意点

取り扱わない話

  • 同期制御と排他制御の各論
  • Thread クラス
  • ReentrantLock クラス
  • Atomic クラス
  • wait()/notify()
  • Executorフレームワーク
  • 不変オブジェクト
    • マルチスレッドにおける不変オブジェクトについては以下の過去記事にて解説してます。

目次

前編は1~3、中編は4~5、後編は6~9 です。

1. SonarLint の警告内容

SonarLint は、コード内の「バグ発見」「脆弱性発見」「メンテナンス困難なコード検出」などをサポートする自動コードレビューツールです。SonarLint の警告内容は以下になります。

[Java] S2696 raises no issue if method is synchronized but not static
// [Java] S2696 メソッドが同期化されているが静的でない場合、問題が発生する。 Make the enclosing method "static" or remove this set.
// メソッドを "static "にするか、このセットを削除してください。


Correctly updating a static field from a non-static method is tricky to get right and could easily lead to bugs if there are multiple class instances and/or multiple threads in play. Ideally, static fields are only updated from synchronized static methods. This rule raises an issue each time a static field is updated from a non-static method. Noncompliant Code Example.
// 静的でないメソッドから静的フィールドを正しく更新するのは難しいことで、複数のクラスインスタンスや複数のスレッドが存在する場合、簡単にバグにつながる可能性があります。理想的には、静的フィールドは同期化された静的メソッドからしか更新されません。 このルールは、静的フィールドが非静的メソッドから更新されるたびに、問題を発生させます。

非準拠のコード例

public class MyClass {
  private static int count = 0;

  public void doSomething() {
    //...
    count++;  // Noncompliant
  }
}

といった内容でした。意味が全く意味がわからないまま、エラー文を元に複数の信頼できるサイトから同じような解決方法を見つけました。以下のコードです。違いはdoSomething()メソッドにstaticsynchronized修飾子が付いているだけです。

[Java] S2696 raises no issue if method is synchronized but not static - Report False-positive / False-negative... - Sonar Community

LCK05-J. 信頼できないコードによって変更されうる static フィールドへのアクセスは同期する

Java static code analysis | Code Smell: "Lock" objects should not be "synchronized"

準拠したコード例

public class MyClass {
  private static int count = 0;

  public static synchronized void doSomething() {
    //...
    count++;  // compliant
  }
}

上記の警告文は些か抽象的でいまいち要領を得ません。より具体的に言うと、
doSomething()メソッドはインスタンスメソッドであり、静的フィールドであるcountを更新している。だが、静的フィールドを更新するには、同期化された静的メソッド内で行う必要があるため、このコードは非準拠である」

ということを言っています。解決するには以下の方法があります。

  1. フィールドを静的から非静的にする。つまり、staticを外して単純なインスタンスフィールドにする。
  2. doSomething()メソッドを静的かつ同期的(synchronized)にする(上記準拠コード)
  3. 静的同期的なセッターメソッドを実装する(以下コード)
public class MyClass {
    private static int count = 0;

    public void doSomething() {
        //...
        setCount(count + 1);
    }
    
    private static synchronized void setCount(int newCount) {
        count = newCount;
    }
}

本記事では二番目の方法を採用したという前提で話をします。どの方法が最善かはフィールドの使用方法によって異なります。静的なフィールドがプログラムのどこかで変更される可能性がある場合、同期化されたsetterメソッドを使用するほうがよいらしい。一方、フィールドが読み取り専用である場合、静的同期的なメソッドを使用することが良いようです。

今回の SonarLint 警告文は、端的に言えば - 「static フィールドへのアクセスは synchronized で static なメソッドにのみ設定されるべきである」

という意味です。なぜそうでなくてはいけないのでしょうか?  

最も一般的に紹介されるのが、競合状態化(スレッドセーフではない環境)における共有データ(クリティカルセクション)のデータ不整合問題です。カウント10000回をマルチスレッドで実行したら6000回くらいの結果になって、期待通り10000回カウントしないとかですね。

またセキュリティ面にも問題があります。悪意ある攻撃者が故意に同期を取らず、もしくは信頼できるユーザが意図せずにフィールドへアクセスすることが可能であるためです。このような状態では、信頼できるユーザがこのクラスを安全に使用することはできません。このセキュアな観点での意味や、SonarLint 警告文を理解するには、以下の事柄を理解する必要があります。

  • static 静的の意味
  • マルチスレッドの基本的な話
  • 排他制御と可視性
  • synchronized 同期的の意味

それでは一つずつ見ていきましょう。

2. プログラム実行には実行内容をメモリ上に展開する必要がある

ほとんどのPCは「ノイマン型」コンピュータです。

よく見るやつです。入力装置はマウスやキーボード、出力装置はディスプレイなどですね。 ノイマン型のコンピュータの特徴は、

  1. プログラム内蔵方式 あらかじめ機械語が主記憶装置(メモリ)に内蔵されている。

  2. 逐次制御方式 主記憶装置(メモリ)に格納されている情報を中央処理装置(CPU)が1つ1つ取り出して実行する。

  3. アドレス指定方式でデータを扱う 固定の命令(機械語)は命令部とアドレス部から構成される。アドレス部は命令の対象となるデータがある主記憶装置(メモリ)のアドレスを示す情報を持っており、 アドレスを示す情報はCPU内部のレジスタ(高速な記憶装置)に格納される。 CPUはレジスタにある場所情報をもとにメモリから順次読み出す。

  4. 固定の命令(機械語)が使える 演算処理や動作などに固定の命令(機械語)を用いることができる。 「実行対象のプログラムをデータとしてメモリ上に展開し、処理演算装置(CPU)はそれを順次読み込んで処理する」

となっています。

つまり、作成したプログラムや第三者が作成したライブラリ、あるいはPC上で動く各種アプリケーションから、サーバ上で動作するWebサービスに至るまでどのようなプログラムも実行時に必ずメモリ上にその内容が展開される仕組みとなっています。メモリとは、プログラムの実行中に取り扱っているデータを一時的に保存する領域です。

実行対象プログラムは、実行前にメモリ上にその内容が展開されます。今回は取り上げませんが、この作業を行なうのはOSの役割です。また、OSとJVMがそれぞれで占有するメモリ空間などの概念もあります。JavaGCの仕組みを理解するのに重要なので興味があれば調べてみて下さい。

2-1. なぜメモリ上に展開するのか?

プロセッサ(CPU)がメモリにアクセスして、プログラムの実行に必要な情報を取得するためです。ではなぜ、プロセッサは補助記憶装置(ハードディスクやSSDなど)直接アクセスしないのでしょうか?

プロセッサはCPU(Central Processing Unit)のことで、コンピュータ内でデータを処理するために使用されるチップです。M1Mac のプロセッサが Intel プロセッサから Apple シリコンに変更されましたね。Apple シリコンもプロセッサです。

メモリはプロセッサにとって最も早いアクセス可能な記憶装置であり、CPUはメモリと高速に通信することができます。一方、補助記憶装置に対するアクセスは、メモリよりもアクセス時間が遅く、処理速度が低下するため、プログラムやデータはメモリに展開されます。プログラムやデータがメモリに展開されることで、プロセッサは高速かつ効率的にアクセスでき、処理速度が向上するというわけです。

3. static の意味

Java には、プログラムが実行される際に使用される複数のメモリ領域があり、主なメモリ領域には以下のようなものがあります。メモリの管理方法やデータの使用目的によってメモリの空間が定義されています。

  1. スタック領域
  2. ヒープ領域
  3. static 領域
  4. コンスタントプール
領域 説明 変数との対応 生存期間
static領域 static 変数・メソッドを管理する。静的領域とも呼ばれ、スレッドから共有される。プログラムのどの部分からでも参照することができるため、プログラム全体で共有する変数や定数を格納する。 クラススコープ内で static キーワード付きで変数宣言を行なう。 クラス利用をするJavaアプリケーションの開始から終了まで有効

静的領域(static領域)は、Javaアプリケーション実行中に領域の大きさ(使用メモリサイズ)が変わらないため、静的領域と呼ばれています。

その他のメモリ空間

領域 説明 変数との対応 生存期間
スタック領域 ローカル変数、パラメータ、戻り値、演算に使われる任意の値などを管理する領域。スタック領域は共有リソースではないため、スレッドセーフ。実際に処理されるデータを格納する領域。「スタック」=積み重ねという名前が表すように、処理対象のデータはFILO(先入れ後出し)方式でデータを管理し、処理が完了したデータはスタックから破棄。 メソッドスコープあるいはforスコープなどの特定の処理スコープ内で定義する。 特定の処理スコープ内だけで有効な変数。変数定義した処理スコープの処理がすべて完了すると破棄。
ヒープ領域 new演算子で生成されたオブジェクトと配列を管理。必要な時に、必要なサイズを指定して領域が確保できる自由度の高いメモリ領域。スレッドで共有される。ただし、確保したメモリは必ず解放する必要がある。Java ではGCにて実行中のプログラムの動作から不要になったと判断した領域を自動的に解放。メモリの解放を明示的に行わなけばならない言語ではメモリリークに注意する必要がある。 クラススコープ内で変数定義を行なう。 対象クラスのインスタンスがnew演算子にて生成されてから、破棄される(明示的に解放するかGC)までの間有効。
static領域 static 変数・メソッドを管理する。静的領域とも呼ばれ、スレッドから共有される。プログラムのどの部分からでも参照することができるため、プログラム全体で共有する変数や定数を格納する。 クラススコープ内で static キーワード付きで変数宣言を行なう。 クラス利用をするJavaアプリケーションの開始から終了まで有効
コンスタントプール 定数値や文字列などのリテラルが格納されるメモリ領域で、スレッドから共有される。コンパイル時に定義され、プログラムの実行中に変更することはできない。そのため、定数領域に格納された値は不変であり、プログラムの安全性やパフォーマンスを向上させる。 クラススコープ内で final キーワード付きでの変数宣言や文字列リテラルでの変数定義を行なう。 クラス利用をするJavaアプリケーションの開始から終了まで有効

以下に static 領域のメモリ空間を簡易的に図式化しました。違いと使い分けを箇条書きします。
※余談: new 演算子にはインスタンス化とコンストラクタによるオブジェクトの初期化という意味がある

image.png

インスタンス化の要否

  • インスタンスメソッドやフィールドはインスタンス化によってメモリ(ヒープ領域)上に展開しないと参照できません。
  • static メソッドやフィールドは static 領域というメモリ上に常に展開されているので、インスタンス化しなくても参照可能です。

3-1. static 領域のフィールド・メソッドはどこからでもアクセスできる

static 領域に定義されたフィールドやメソッドは、条件付きですがどこからでも参照可能です。クラス自体がロードされていれば、インスタンス生成の有無や値に関わらず、static フィールドや static メソッドを参照できます。ただし、アクセス修飾子によってアクセス制限がかけられる場合もあります。

  • 同じパッケージ内のクラスから参照
    • private でなければ 参照可能
      • package private / protected / public は可能
      • protected の場合は、同一パッケージまたはサブクラスからのみ参照可能
  • 異なるパッケージに属するクラスからの参照
    • public であれば参照可能
public class MyClass {
    private static int count = 0;

    public static void increment() {
        count++;
    }
}

他のクラスからアクセスする際には、以下のようにクラス名を指定して参照可能です。オブジェクトをインスタンス化する必要はありません。

MyClass.increment();

3-2. アクセスできるフィールドの違い

  1. static メソッドから static フィールドを使う
  2. static メソッドからインスタンスフィールドを使う
  3. インスタンスメソッドから static フィールドを使う
  4. インスタンスメソッドからインスタンスフィールドを使う

上記の2つ目の選択肢、「static メソッドからインスタンスフィールドを使う」は不可能です。static 領域に定義されたフィールドやメソッドは、条件付きですがどこからでも参照可能ですが、static メソッドからインスタンスフィールドを使うことはできません。 これは static 領域と、ヒープ領域のインスタンスの関係は、別々に独立した領域になっていることに起因します。

インスタンスは「自分のクラスが何クラスか」を知っています。だからthisで自分自身を指し示すことができます。なので、インスタンス化されたクラスが static なメソッドやフィールドを持つクラスをインポートしていれば、アクセス出来ます。自分自身が依存しているクラスですから当然です。ですが、static 領域はインスタンス管理・依存しているわけではありません。static 領域にとって、同じクラスのインスタンスはたくさんあって、static 領域はそれらインスタンスの判別はできません。参照されることはあっても何から参照されているかは知りようがないのです。これは Singleton やグローバル変数も同じです。static メソッドはクラスに関連付けられているため、クラスに依存関係(import)があればその、クラス内の任意の場所からアクセスできます。インスタンスフィールドはクラスに関連付けられているわけではなく、インスタンスに関連付けられているため、static メソッドからアクセスすることはできません。

3-3. それぞれのフィールドの違い

  • static フィールド:インスタンス毎に異なる値に対してアクセスする必要がない場合や、共有・参照されるべき値を保持する
  • インスタンスフィールド: インスタンス毎で異なる(必要な)値を保持する

そのため、static メソッドやフィールドは、オブジェクトの状態に依存しないため、ユーティリティクラスやユーティリティメソッドの実装に適しています。また、定数や共有変数を扱う場合にも便利です。

3-4. static メソッドとインスタンスメソッドの使い分け

  • static メソッド:インスタンス毎に異なる様に持たせた値に対してアクセスする必要がない、共有・参照しなければならない場合
  • インスタンスメソッド:インスタンス毎に異なる様に持たせた値に対してアクセスをしたい場合
  • フィールドにアクセスしない場合はケースバイケース

3-5. static メソッド・static フィールドの使い所

  1. 全てのインスタンスが共有して使えるメンバフィールドやメソッドを定義したい場合
    1. 状態に依存しない値、定数など
  2. クラスに関連する共通なメソッドをまとめたい場合
    1. ex.Integer.parseInt()

上記の図のように、static 領域はヒープ領域とは別のメモリ空間なので、インスタンスフィールドの状態と static フィールドの状態は別々に管理されています。なので、どのインスタンスから参照しても同じ状態を参照するようにしたい場合などが挙げられます。下記にメソッドとフィールド別の使い所を記載します。

3-5-1. static メソッドの使い所

staticメソッドは、クラス全体に関係する共通の処理を行うために使用されます。 

  1. エントリーポイント定義:アプリケーションエントリーポイントとして使用される main()メソッドは、必ず static メソッドで定義されます。その様な言語使用になっています。
  2. ユーティリティメソッドインスタンスを必要としない共通の処理を実行する場合に利用されます。例えば、Math クラスの abs メソッドは、引数に渡された数値の絶対値を返します。このような処理は、インスタンス化する必要がないため、static メソッドとして実装されます。
  3. ファクトリメソッドインスタンスを作成するためのメソッドを static メソッドとして定義することができます。例えば、Java の Collections クラスには、空のリストやイテレーターなどを作成するための static メソッドが用意されています。
     ~~~中略~~~

package java.util;

     ~~~中略~~~

public class Collections {
    // Suppresses default constructor, ensuring non-instantiability.
    private Collections() {
    }

     ~~~中略~~~

    /**
     * The empty list (immutable).  This list is serializable.
     *
     * @see #emptyList()
     */
    @SuppressWarnings("rawtypes")
    public static final List EMPTY_LIST = new EmptyList<>();

    /**
     * Returns an empty list (immutable).  This list is serializable.
     *
     * <p>This example illustrates the type-safe way to obtain an empty list:
     * <pre>
     *     List&lt;String&gt; s = Collections.emptyList();
     * </pre>
     *
     * @implNote
     * Implementations of this method need not create a separate {@code List}
     * object for each call.   Using this method is likely to have comparable
     * cost to using the like-named field.  (Unlike this method, the field does
     * not provide type safety.)
     *
     * @param <T> type of elements, if there were any, in the list
     * @return an empty immutable list
     *
     * @see #EMPTY_LIST
     * @since 1.5
     */
    @SuppressWarnings("unchecked")
    public static final <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;
    }

     ~~~中略~~~
}

3-5-2. static フィールドの使い所

  1. 定数:static フィールドを定数として定義することができます。例えば、Java の Math クラスには、πの値を表す static フィールドが定義されています。
  2. 共有データ:クラスの全てのインスタンスで共有されるデータを static フィールドとして定義することができます。例えば、Java の Singleton パターンでは、インスタンスが一つしか存在しないことを保証するために、private で static なインスタンスを持つように実装されます。環境変数などが定義されたファイルやインスタンスなどの様に一度決まれば変更されない、もしくは変更される場合にそれを参照する対象全てが同期して欲しいものなど

インスタンスにはインスタンスに必要なフィールドしかない(のが理想的なカプセル化)はずです。つまりインスタンスメソッドとはインスタンスフィールドにアクセスをするのが本来の目的の一つです。それらを踏まえると、それぞれのメソッドがアクセスしたいフィールドは異なることが解ります。インスタンスメソッドがインスタンスフィールドを使用していない場合は、そのメソッドをそのクラスに置く必要はありません。インスタンスフィールドとは「インスタンスごとで異なる値・状態をもつ」ためにあります。インスタンスメソッド内で使用していないのであれば、意味がありません。対象ロジックの置き場所を再検討するべきサインです。逆に static なフィールドが特定の単一のオブジェクトからしかアクセスされていない場合は不適切な static フィールドであることを意味しています。static フィールドはみんなから参照されるべき共有値だからです。特定のオブジェクトからしかアクセスされないのであればインスタンスフィールドにするべきです。

また、以下の二点はパフォーマンスの観点から理解しておくべきです。

  1. static 領域のメソッドやフィールドは常に高速にアクセスできるということ
  2. static フィールドや static メソッドが多用されると、メモリ(static 領域)使用量が増加し、パフォーマンスが低下する可能性があるため、適切な使用方法を考慮する必要があること

3-6. static メソッドやフィールドは、オブジェクトの状態に依存しない

static メソッドやフィールドは、オブジェクトの状態に依存しないというのはどういう意味でしょうか。

インスタンスメソッドやフィールドは、インスタンスごとに異なる状態を持つため、オブジェクトをインスタンス化する必要があります。それに対して、static 領域にあるメソッドやフィールドは、プログラムの開始から終了まで、常にメモリに展開されています。つまり、オブジェクトのインスタンス化やメソッドの呼び出しに関係なく、プログラムが実行されている間は常にメモリ上に展開されています。

static メソッドやフィールドは、クラス自体に紐づいているため、インスタンス化されたオブジェクトの状態に依存しません。インスタンス化するのはインスタンスごとに異なる値を持つインスタンスフィールドが必要だからです。異なる値に依存する、つまり、状態に依存するのがインスタンスです。それに対して、static フィールドは常に定まった値を取り扱います。static メソッドは同じ引数に対して常に同じ値が返されます。ちなみに、この様な性質を関数型言語の世界では参照透過性と呼びます。インスタンスメソッドやインスタンスフィールドは参照透過性を持ちません。これらの要素は、インスタンスの状態に依存しており、同じ引数を渡しても、オブジェクトの状態が異なる場合は異なる結果を返す可能性があるからです。

static メソッドやフィールドはクラス自体に紐づいているため、非 static なオブジェクトやメソッドなどからはクラス単位で共有されています。MyClass.increment();という形で呼び出せます。

注意点として、static フィールドや static メソッドはクラス単位で共有されるため、複数のオブジェクトが同じ static フィールドを参照している状態が常に起こり得ます。その場合、一方のオブジェクトが static フィールドの値を変更すると、他方のオブジェクトにも影響が及びます。そのため、static フィールドはスレッドセーフな必要があります。

余談:参照透過性と Stream API
参照透過性 (referential transparency) とは、プログラムの評価結果が、その式における同じ引数によって常に参照透過性とは、同じ入力に対して常に同じ出力を返す関数やメソッドのことを指します。つまり、副作用を持たず、入力に対して完全に予測可能な振る舞いをすることを要求します。参照透過性がある関数やメソッドは、その結果をキャッシュしたり、並列処理したりすることができるため、プログラムの効率性を向上させることができます。StreamAPIなどはこの性質が取り入れられています。

例えば、以下の式は参照透過性を持ちます。

x + y

これは、与えられた同じ xy に対して、必ず同じ結果を返します。

一方、以下の式は参照透過性を持ちません。

getRandomNumber()

これは、呼び出すたびにランダムな値を返すため、同じ引数であっても呼び出すたびに結果が異なります。

参照透過性がある関数や式は、プログラムの理解や変更、テスト、最適化などの面で非常に有用です。また、関数型プログラミングにおいては参照透過性が重視されます。

StreamAPIなどはこの性質が取り入れられています。Stream APIには、ストリーム生成、中間操作、終端操作の3つの段階があります。ストリーム生成ではデータソースからストリームを生成し、中間操作は新しいStreamを返し、終端操作はStreamから結果を取得します。中間操作は入力Streamを変更せず、新しいStreamを生成するため、参照透過性を持ちます。また、終端操作は常に同じ入力に対して同じ結果を返すため、参照透過性を持ちます。このように、Stream API関数型プログラミングの考え方に基づいて設計されており、参照透過性が基本的には保証されています。プログラムの中で状態を変更せずにデータを扱うことができ、より安全で保守性の高いコードを書くことができます。

この「スレッドセーフな必要がる」という点が、今回の SonarLint の警告内容の理解に必要な重要知識となります。

3-7. staticメソッドの注意点

3-7-1. 参照共有・アンスレッドセーフ問題

上記でも記述しましたが、static フィールドや static メソッドはクラスに紐づくことから、他のオブジェクトからはクラス単位で共有されます。共有する方法はクラスに依存関係を追加(import)すればいいわけです。複数のオブジェクトが同じ static フィールドを参照している状態が常に起こり得ます。その場合、一方のオブジェクトが static フィールドの値を変更すると、他方のオブジェクトにも影響が及びます。そのため、static フィールドはスレッドセーフな必要があります。スレッドセーフについては後述します。今回の SonarLint 警告の中核はこの問題です。

つまり、1 つのスレッドが static フィールドを変更しているときに、別のスレッドがそのフィールドにアクセスしても、不正な結果にならないようにする必要があります。不正な結果とは、期待した結果以外の結果です。たとえば、count フィールドを 1 回インクリメントする予定が、2 回インクリメントされた場合、これは不正な結果です。これは、1 つのスレッドがフィールドを変更しているときに、別のスレッドがフィールドにアクセスしたために発生する現象です。

マルチスレッドをスレッドセーフにするためには一般的に、synchronized や volatile などの手段を使って、適切に排他制御を行う必要があります。それと同じく static フィールドの値を変更する場合も同じく、他のスレッドとの競合を避けるために、適切な排他制御を行う必要があります。

マルチスレッドについては次の章から説明します。 

3-7-2. DIがしづらくテスタビリティが低い(おまけ:本編関係なし)

DI(Dependency Injection)とは、オブジェクトに依存関係を注入する手法です。依存関係とは、オブジェクトが正常に機能するために必要なオブジェクトやデータのことです。DIは「依存性注入」と訳されますがより実態を示しているのは「依存性オブジェクトの注入」ではないかと思います。より平たく言えば、「そのクラスが依存しているオブジェクトの外部からのインスタンス注入」となります。

  1. モックやスタブの注入: DIは、依存関係を注入するメカニズムを提供します。これにより、ユニットテスト中にモックやスタブの実装を注入することができます。モックやスタブは、テスト中に予測可能な動作や返り値を提供することができます。これにより、テストケースの制御が容易になり、テストの信頼性を高めることができます。

  2. 依存関係の分離: DIによって依存関係が明示的に宣言されるため、クラス間の結合度が低くなります。これにより、単体のクラスをユニットテストする際に、そのクラスが依存する他のクラスやリソースの実装詳細を意識する必要がありません。代わりに、依存関係をモックやスタブに置き換えることができます。

  3. テストの容易な再構成: DIを使用すると、依存関係の注入が外部で行われるため、テスト中に異なる実装を注入することができます。例えば、本番環境では実際のデータベースを使用するが、テスト環境ではインメモリデータベースを使用するなどの設定が可能です。これにより、テストの再構成が容易になります。

static メソッドは、インスタンスメソッドとは異なり、クラス自体に関連付けられているメソッドであり、インスタンスを生成せずに呼び出すことができるメソッドでもあるため、DIが困難となります。また、static メソッドは、単体テストも困難です。単体テストでは、オブジェクトをモック(代役)に置き換えてテストを行う必要があります。DIはオブジェクトに依存関係を注入する手法であり、モックはテスト中に依存関係をシミュレートするために使用できるオブジェクトです。 

モックを生成する際には、DIでテスト対象のインスタンスメソッドがあるインスタンスを生成し依存関係を設定できる状態することでモックの準備が整います。ですが、上記で紹介した様に、インスタンスメソッドはヒープ領域にクラスデータを展開することでアクセスできます。対して、static メソッドは static 領域に展開されます。このようなメモリ領域の関係により、DIがしづらく、よって、モックに置き換えることも面倒というわけです。

DIはデザインパターン
解決したい課題しては、ロジック層とDB層など外部保存層との依存性逆転も目的としています。簡単に言えばロジック層→DB層に向いている依存方向をロジック層←DB層にするためです。これによりDBなどの変更にロジック層が影響されることがなくなります。ビジネスの中核はビジネスロジックです。これが、データの保管方法に影響されるなんてのはおかしな話やないか、ということですね。

引用元:terasoluna.org 2.4. アプリケーションのレイヤ化 スクリーンショット 2023-05-28 18.56.31.png

あとは、いろんな議論でユニットテストの普及目的で導入された行ったけど実はそんなに効果がなかったとかいろんな議論がありますが、二年目のひよっこの自分には全く意味わかりません。

3-7-3. static メソッドのユニッテスト方法(おまけ:本編関係なし)

実は Mock で対応してます。対応以前はどうやってたんでしょうかね。多分以下で紹介する一番目のやり方でしょうか? PowerMock でも出来たみたいですが、JUnit5って実は PowerMock 使えないんですよね、、、。だから、private メソッドのMock化とかも出来ません。そして、今から JUnit 入れるとかであれば JUnit4 をわざわざ選択する必要ってないと思います。いつサポート切れるか分かりません。

  1. 静的メソッドをラップするインスタンスメソッドを作成する
  2. モックライブラリを使用して静的メソッドをモックする

以下は「静的メソッドをラップするインスタンスメソッドを作成する」

ぶっちゃけ、やってられんですよねこんなの。テストの為だけにラップするって何?という思いが禁じ得ないのですが、使用すべき場面ってあるのでしょうか??

public class Sample {

    private static String getFullName(String firstName, String lastName) {
        return String.format("%s %s", firstName, lastName);
    }

    public String getFullName(String firstName) {
        return getFullName(firstName, "");
    }
}

以下は「モックライブラリを使用して静的メソッドをモックする」

@Test
public void testSample() {
    // Mockito を使用して、Sample クラスのモックオブジェクト作成
    try (MockedStatic mocked = mockStatic(Sample.class)) {
      // `Math` クラスの `random()` 静的メソッドの期待値を設定
      mocked.when(Sample::method).thenReturn("bar");
      // 静的メソッドを呼び出す
      assertEquals("bar", Sample.method());
      // Mockito を使用して、静的メソッドの期待を検証
      mocked.verify(Foo::Sample);
      // もし、try-with-resources で実装しない場合、明示的に close() を呼ぶ 
      // 以下の公式サイトでは try-with-resources を推奨
//      mocked.close();
    }

    assertEquals("foo", Foo.Sample());
}

参考:Package org.mockito Class Mockito「48. Mocking static methods (since 3.4.0)」

3-8. 改めて SonarLint の警告内容を見てみましょう

[Java] S2696 メソッドが同期化されているが静的でない場合、問題が発生する。 メソッドを "static "にするか、このセットを削除してください。

静的でないメソッドから静的フィールドを正しく更新するのは難しいことで、複数のクラスインスタンスや複数のスレッドが存在する場合、簡単にバグにつながる可能性があります。理想的には、静的フィールドは同期化された静的メソッドからしか更新されません。 このルールは、静的フィールドが非静的メソッドから更新されるたびに、問題を発生させます。

この警告が言っているのは、複数のインスタンスで共有されているような 静的なフィールドが、非静的なメソッド(つまりインスタンスメソッド)から状態を変更されてしまうと、変更前の値を参照したいはずの無関係な他のインスタンスにも影響が出る状態やから、その状態をなんとかせい、と言っているわけですね。

その解決策として、非静的なメソッドを同期的なメソッドに変更するというものでした。つまりsynchronizedをメソッドにつけるというものでした。

この、synchronizedをつけると何が起きるのでしょうか?

  1. 排他制御が行われる様になる
  2. 排他制御としてsynchronizedミューテックスである
  3. 複数スレッドが同時にsynchronizedブロックにアクセスしても、操作は一度に一つのスレッドのみとなりアトミックとなる。
  4. 複数スレッドが同時にsynchronizedブロックにアクセスしても、排他制御によりスレッドセーフとなる。
  5. synchronizedブロック内でのクリティカルセクションの変更結果が他のスレッドに対して可視になる。
  6. ただし、ミューテックスの使用箇所によってはパフォーマンスを低下させる可能性が高い

いやぁ、すごいですねsynchronized。実はsynchronizedだけでは、実務上で様々な問題を引き起こすので「スレッドセーフ?synchronizedつけといたらいいんでしょ?」みたいな感じで適当にやっていると致命的なパフォーマンス低下や最悪システムの停止につながる様なデッドロックを引き起こす可能性もあります。ただし、本記事ではそこまで説明しません。この解説の粒度でやっていたらとんでもない長さになります。それに実務的知見も全くないので、またいつかやりたいところです。

以降では上記で挙げたsynchronizedキーワードを付けることで起こること、の解説の為に必要な基礎知識について解説します。とんでもなく多いです。量が。申し訳ない。簡潔さと詳細さを取る場合、私は詳細さを取る人間です。

余談:やや不正確な表現だったものの補足

「static 領域にあるメソッドやフィールドは、プログラムの開始から終了まで、常にメモリに展開されています」は、より正確には、JVMは動的にメモリを割り当てるため、static 領域にあるメソッドやフィールドが常にメモリ上に展開されていることは、やや不正確です。

続きは

  • 『中編:マルチスレッドの基本的な用語と概念』

static と synchronized から始めるマルチスレッド入門 〜中編〜 - よしたろうブログ

  • 『後編:synchronized の使い方と注意点』

static と synchronized から始めるマルチスレッド入門 〜後編〜 - よしたろうブログ